#
tokens: 49817/50000 5/975 files (page 44/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 44 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .changeset
│   ├── config.json
│   └── README.md
├── .claude
│   ├── commands
│   │   └── dedupe.md
│   └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│   └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│   ├── mcp.json
│   └── rules
│       ├── ai_providers.mdc
│       ├── ai_services.mdc
│       ├── architecture.mdc
│       ├── changeset.mdc
│       ├── commands.mdc
│       ├── context_gathering.mdc
│       ├── cursor_rules.mdc
│       ├── dependencies.mdc
│       ├── dev_workflow.mdc
│       ├── git_workflow.mdc
│       ├── glossary.mdc
│       ├── mcp.mdc
│       ├── new_features.mdc
│       ├── self_improve.mdc
│       ├── tags.mdc
│       ├── taskmaster.mdc
│       ├── tasks.mdc
│       ├── telemetry.mdc
│       ├── test_workflow.mdc
│       ├── tests.mdc
│       ├── ui.mdc
│       └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   ├── enhancements---feature-requests.md
│   │   └── feedback.md
│   ├── PULL_REQUEST_TEMPLATE
│   │   ├── bugfix.md
│   │   ├── config.yml
│   │   ├── feature.md
│   │   └── integration.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── scripts
│   │   ├── auto-close-duplicates.mjs
│   │   ├── backfill-duplicate-comments.mjs
│   │   ├── check-pre-release-mode.mjs
│   │   ├── parse-metrics.mjs
│   │   ├── release.mjs
│   │   ├── tag-extension.mjs
│   │   ├── utils.mjs
│   │   └── validate-changesets.mjs
│   └── workflows
│       ├── auto-close-duplicates.yml
│       ├── backfill-duplicate-comments.yml
│       ├── ci.yml
│       ├── claude-dedupe-issues.yml
│       ├── claude-docs-trigger.yml
│       ├── claude-docs-updater.yml
│       ├── claude-issue-triage.yml
│       ├── claude.yml
│       ├── extension-ci.yml
│       ├── extension-release.yml
│       ├── log-issue-events.yml
│       ├── pre-release.yml
│       ├── release-check.yml
│       ├── release.yml
│       ├── update-models-md.yml
│       └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│   ├── hooks
│   │   ├── tm-code-change-task-tracker.kiro.hook
│   │   ├── tm-complexity-analyzer.kiro.hook
│   │   ├── tm-daily-standup-assistant.kiro.hook
│   │   ├── tm-git-commit-task-linker.kiro.hook
│   │   ├── tm-pr-readiness-checker.kiro.hook
│   │   ├── tm-task-dependency-auto-progression.kiro.hook
│   │   └── tm-test-success-task-completer.kiro.hook
│   ├── settings
│   │   └── mcp.json
│   └── steering
│       ├── dev_workflow.md
│       ├── kiro_rules.md
│       ├── self_improve.md
│       ├── taskmaster_hooks_workflow.md
│       └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│   ├── CLAUDE.md
│   ├── config.json
│   ├── docs
│   │   ├── autonomous-tdd-git-workflow.md
│   │   ├── MIGRATION-ROADMAP.md
│   │   ├── prd-tm-start.txt
│   │   ├── prd.txt
│   │   ├── README.md
│   │   ├── research
│   │   │   ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│   │   │   ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│   │   │   ├── 2025-06-14_test-save-functionality.md
│   │   │   ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│   │   │   └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│   │   ├── task-template-importing-prd.txt
│   │   ├── tdd-workflow-phase-0-spike.md
│   │   ├── tdd-workflow-phase-1-core-rails.md
│   │   ├── tdd-workflow-phase-1-orchestrator.md
│   │   ├── tdd-workflow-phase-2-pr-resumability.md
│   │   ├── tdd-workflow-phase-3-extensibility-guardrails.md
│   │   ├── test-prd.txt
│   │   └── tm-core-phase-1.txt
│   ├── reports
│   │   ├── task-complexity-report_autonomous-tdd-git-workflow.json
│   │   ├── task-complexity-report_cc-kiro-hooks.json
│   │   ├── task-complexity-report_tdd-phase-1-core-rails.json
│   │   ├── task-complexity-report_tdd-workflow-phase-0.json
│   │   ├── task-complexity-report_test-prd-tag.json
│   │   ├── task-complexity-report_tm-core-phase-1.json
│   │   ├── task-complexity-report.json
│   │   └── tm-core-complexity.json
│   ├── state.json
│   ├── tasks
│   │   ├── task_001_tm-start.txt
│   │   ├── task_002_tm-start.txt
│   │   ├── task_003_tm-start.txt
│   │   ├── task_004_tm-start.txt
│   │   ├── task_007_tm-start.txt
│   │   └── tasks.json
│   └── templates
│       ├── example_prd_rpg.md
│       └── example_prd.md
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── apps
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── command-registry.ts
│   │   │   ├── commands
│   │   │   │   ├── auth.command.ts
│   │   │   │   ├── autopilot
│   │   │   │   │   ├── abort.command.ts
│   │   │   │   │   ├── commit.command.ts
│   │   │   │   │   ├── complete.command.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next.command.ts
│   │   │   │   │   ├── resume.command.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── start.command.ts
│   │   │   │   │   └── status.command.ts
│   │   │   │   ├── briefs.command.ts
│   │   │   │   ├── context.command.ts
│   │   │   │   ├── export.command.ts
│   │   │   │   ├── list.command.ts
│   │   │   │   ├── models
│   │   │   │   │   ├── custom-providers.ts
│   │   │   │   │   ├── fetchers.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── prompts.ts
│   │   │   │   │   ├── setup.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── next.command.ts
│   │   │   │   ├── set-status.command.ts
│   │   │   │   ├── show.command.ts
│   │   │   │   ├── start.command.ts
│   │   │   │   └── tags.command.ts
│   │   │   ├── index.ts
│   │   │   ├── lib
│   │   │   │   └── model-management.ts
│   │   │   ├── types
│   │   │   │   └── tag-management.d.ts
│   │   │   ├── ui
│   │   │   │   ├── components
│   │   │   │   │   ├── cardBox.component.ts
│   │   │   │   │   ├── dashboard.component.ts
│   │   │   │   │   ├── header.component.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next-task.component.ts
│   │   │   │   │   ├── suggested-steps.component.ts
│   │   │   │   │   └── task-detail.component.ts
│   │   │   │   ├── display
│   │   │   │   │   ├── messages.ts
│   │   │   │   │   └── tables.ts
│   │   │   │   ├── formatters
│   │   │   │   │   ├── complexity-formatters.ts
│   │   │   │   │   ├── dependency-formatters.ts
│   │   │   │   │   ├── priority-formatters.ts
│   │   │   │   │   ├── status-formatters.spec.ts
│   │   │   │   │   └── status-formatters.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── layout
│   │   │   │       ├── helpers.spec.ts
│   │   │   │       └── helpers.ts
│   │   │   └── utils
│   │   │       ├── auth-helpers.ts
│   │   │       ├── auto-update.ts
│   │   │       ├── brief-selection.ts
│   │   │       ├── display-helpers.ts
│   │   │       ├── error-handler.ts
│   │   │       ├── index.ts
│   │   │       ├── project-root.ts
│   │   │       ├── task-status.ts
│   │   │       ├── ui.spec.ts
│   │   │       └── ui.ts
│   │   ├── tests
│   │   │   ├── integration
│   │   │   │   └── commands
│   │   │   │       └── autopilot
│   │   │   │           └── workflow.test.ts
│   │   │   └── unit
│   │   │       ├── commands
│   │   │       │   ├── autopilot
│   │   │       │   │   └── shared.test.ts
│   │   │       │   ├── list.command.spec.ts
│   │   │       │   └── show.command.spec.ts
│   │   │       └── ui
│   │   │           └── dashboard.component.spec.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── docs
│   │   ├── archive
│   │   │   ├── ai-client-utils-example.mdx
│   │   │   ├── ai-development-workflow.mdx
│   │   │   ├── command-reference.mdx
│   │   │   ├── configuration.mdx
│   │   │   ├── cursor-setup.mdx
│   │   │   ├── examples.mdx
│   │   │   └── Installation.mdx
│   │   ├── best-practices
│   │   │   ├── advanced-tasks.mdx
│   │   │   ├── configuration-advanced.mdx
│   │   │   └── index.mdx
│   │   ├── capabilities
│   │   │   ├── cli-root-commands.mdx
│   │   │   ├── index.mdx
│   │   │   ├── mcp.mdx
│   │   │   ├── rpg-method.mdx
│   │   │   └── task-structure.mdx
│   │   ├── CHANGELOG.md
│   │   ├── command-reference.mdx
│   │   ├── configuration.mdx
│   │   ├── docs.json
│   │   ├── favicon.svg
│   │   ├── getting-started
│   │   │   ├── api-keys.mdx
│   │   │   ├── contribute.mdx
│   │   │   ├── faq.mdx
│   │   │   └── quick-start
│   │   │       ├── configuration-quick.mdx
│   │   │       ├── execute-quick.mdx
│   │   │       ├── installation.mdx
│   │   │       ├── moving-forward.mdx
│   │   │       ├── prd-quick.mdx
│   │   │       ├── quick-start.mdx
│   │   │       ├── requirements.mdx
│   │   │       ├── rules-quick.mdx
│   │   │       └── tasks-quick.mdx
│   │   ├── introduction.mdx
│   │   ├── licensing.md
│   │   ├── logo
│   │   │   ├── dark.svg
│   │   │   ├── light.svg
│   │   │   └── task-master-logo.png
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── style.css
│   │   ├── tdd-workflow
│   │   │   ├── ai-agent-integration.mdx
│   │   │   └── quickstart.mdx
│   │   ├── vercel.json
│   │   └── whats-new.mdx
│   ├── extension
│   │   ├── .vscodeignore
│   │   ├── assets
│   │   │   ├── banner.png
│   │   │   ├── icon-dark.svg
│   │   │   ├── icon-light.svg
│   │   │   ├── icon.png
│   │   │   ├── screenshots
│   │   │   │   ├── kanban-board.png
│   │   │   │   └── task-details.png
│   │   │   └── sidebar-icon.svg
│   │   ├── CHANGELOG.md
│   │   ├── components.json
│   │   ├── docs
│   │   │   ├── extension-CI-setup.md
│   │   │   └── extension-development-guide.md
│   │   ├── esbuild.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   ├── package.mjs
│   │   ├── package.publish.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── components
│   │   │   │   ├── ConfigView.tsx
│   │   │   │   ├── constants.ts
│   │   │   │   ├── TaskDetails
│   │   │   │   │   ├── AIActionsSection.tsx
│   │   │   │   │   ├── DetailsSection.tsx
│   │   │   │   │   ├── PriorityBadge.tsx
│   │   │   │   │   ├── SubtasksSection.tsx
│   │   │   │   │   ├── TaskMetadataSidebar.tsx
│   │   │   │   │   └── useTaskDetails.ts
│   │   │   │   ├── TaskDetailsView.tsx
│   │   │   │   ├── TaskMasterLogo.tsx
│   │   │   │   └── ui
│   │   │   │       ├── badge.tsx
│   │   │   │       ├── breadcrumb.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── collapsible.tsx
│   │   │   │       ├── CollapsibleSection.tsx
│   │   │   │       ├── dropdown-menu.tsx
│   │   │   │       ├── label.tsx
│   │   │   │       ├── scroll-area.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       ├── shadcn-io
│   │   │   │       │   └── kanban
│   │   │   │       │       └── index.tsx
│   │   │   │       └── textarea.tsx
│   │   │   ├── extension.ts
│   │   │   ├── index.ts
│   │   │   ├── lib
│   │   │   │   └── utils.ts
│   │   │   ├── services
│   │   │   │   ├── config-service.ts
│   │   │   │   ├── error-handler.ts
│   │   │   │   ├── notification-preferences.ts
│   │   │   │   ├── polling-service.ts
│   │   │   │   ├── polling-strategies.ts
│   │   │   │   ├── sidebar-webview-manager.ts
│   │   │   │   ├── task-repository.ts
│   │   │   │   ├── terminal-manager.ts
│   │   │   │   └── webview-manager.ts
│   │   │   ├── test
│   │   │   │   └── extension.test.ts
│   │   │   ├── utils
│   │   │   │   ├── configManager.ts
│   │   │   │   ├── connectionManager.ts
│   │   │   │   ├── errorHandler.ts
│   │   │   │   ├── event-emitter.ts
│   │   │   │   ├── logger.ts
│   │   │   │   ├── mcpClient.ts
│   │   │   │   ├── notificationPreferences.ts
│   │   │   │   └── task-master-api
│   │   │   │       ├── cache
│   │   │   │       │   └── cache-manager.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── mcp-client.ts
│   │   │   │       ├── transformers
│   │   │   │       │   └── task-transformer.ts
│   │   │   │       └── types
│   │   │   │           └── index.ts
│   │   │   └── webview
│   │   │       ├── App.tsx
│   │   │       ├── components
│   │   │       │   ├── AppContent.tsx
│   │   │       │   ├── EmptyState.tsx
│   │   │       │   ├── ErrorBoundary.tsx
│   │   │       │   ├── PollingStatus.tsx
│   │   │       │   ├── PriorityBadge.tsx
│   │   │       │   ├── SidebarView.tsx
│   │   │       │   ├── TagDropdown.tsx
│   │   │       │   ├── TaskCard.tsx
│   │   │       │   ├── TaskEditModal.tsx
│   │   │       │   ├── TaskMasterKanban.tsx
│   │   │       │   ├── ToastContainer.tsx
│   │   │       │   └── ToastNotification.tsx
│   │   │       ├── constants
│   │   │       │   └── index.ts
│   │   │       ├── contexts
│   │   │       │   └── VSCodeContext.tsx
│   │   │       ├── hooks
│   │   │       │   ├── useTaskQueries.ts
│   │   │       │   ├── useVSCodeMessages.ts
│   │   │       │   └── useWebviewHeight.ts
│   │   │       ├── index.css
│   │   │       ├── index.tsx
│   │   │       ├── providers
│   │   │       │   └── QueryProvider.tsx
│   │   │       ├── reducers
│   │   │       │   └── appReducer.ts
│   │   │       ├── sidebar.tsx
│   │   │       ├── types
│   │   │       │   └── index.ts
│   │   │       └── utils
│   │   │           ├── logger.ts
│   │   │           └── toast.ts
│   │   └── tsconfig.json
│   └── mcp
│       ├── CHANGELOG.md
│       ├── package.json
│       ├── src
│       │   ├── index.ts
│       │   ├── shared
│       │   │   ├── types.ts
│       │   │   └── utils.ts
│       │   └── tools
│       │       ├── autopilot
│       │       │   ├── abort.tool.ts
│       │       │   ├── commit.tool.ts
│       │       │   ├── complete.tool.ts
│       │       │   ├── finalize.tool.ts
│       │       │   ├── index.ts
│       │       │   ├── next.tool.ts
│       │       │   ├── resume.tool.ts
│       │       │   ├── start.tool.ts
│       │       │   └── status.tool.ts
│       │       ├── README-ZOD-V3.md
│       │       └── tasks
│       │           ├── get-task.tool.ts
│       │           ├── get-tasks.tool.ts
│       │           └── index.ts
│       ├── tsconfig.json
│       └── vitest.config.ts
├── assets
│   ├── .windsurfrules
│   ├── AGENTS.md
│   ├── claude
│   │   └── TM_COMMANDS_GUIDE.md
│   ├── config.json
│   ├── env.example
│   ├── example_prd_rpg.txt
│   ├── example_prd.txt
│   ├── GEMINI.md
│   ├── gitignore
│   ├── kiro-hooks
│   │   ├── tm-code-change-task-tracker.kiro.hook
│   │   ├── tm-complexity-analyzer.kiro.hook
│   │   ├── tm-daily-standup-assistant.kiro.hook
│   │   ├── tm-git-commit-task-linker.kiro.hook
│   │   ├── tm-pr-readiness-checker.kiro.hook
│   │   ├── tm-task-dependency-auto-progression.kiro.hook
│   │   └── tm-test-success-task-completer.kiro.hook
│   ├── roocode
│   │   ├── .roo
│   │   │   ├── rules-architect
│   │   │   │   └── architect-rules
│   │   │   ├── rules-ask
│   │   │   │   └── ask-rules
│   │   │   ├── rules-code
│   │   │   │   └── code-rules
│   │   │   ├── rules-debug
│   │   │   │   └── debug-rules
│   │   │   ├── rules-orchestrator
│   │   │   │   └── orchestrator-rules
│   │   │   └── rules-test
│   │   │       └── test-rules
│   │   └── .roomodes
│   ├── rules
│   │   ├── cursor_rules.mdc
│   │   ├── dev_workflow.mdc
│   │   ├── self_improve.mdc
│   │   ├── taskmaster_hooks_workflow.mdc
│   │   └── taskmaster.mdc
│   └── scripts_README.md
├── bin
│   └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│   ├── chats
│   │   ├── add-task-dependencies-1.md
│   │   └── max-min-tokens.txt.md
│   ├── fastmcp-core.txt
│   ├── fastmcp-docs.txt
│   ├── MCP_INTEGRATION.md
│   ├── mcp-js-sdk-docs.txt
│   ├── mcp-protocol-repo.txt
│   ├── mcp-protocol-schema-03262025.json
│   └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│   ├── claude-code-integration.md
│   ├── CLI-COMMANDER-PATTERN.md
│   ├── command-reference.md
│   ├── configuration.md
│   ├── contributor-docs
│   │   ├── testing-roo-integration.md
│   │   └── worktree-setup.md
│   ├── cross-tag-task-movement.md
│   ├── examples
│   │   ├── claude-code-usage.md
│   │   └── codex-cli-usage.md
│   ├── examples.md
│   ├── licensing.md
│   ├── mcp-provider-guide.md
│   ├── mcp-provider.md
│   ├── migration-guide.md
│   ├── models.md
│   ├── providers
│   │   ├── codex-cli.md
│   │   └── gemini-cli.md
│   ├── README.md
│   ├── scripts
│   │   └── models-json-to-markdown.js
│   ├── task-structure.md
│   └── tutorial.md
├── images
│   ├── hamster-hiring.png
│   └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│   ├── server.js
│   └── src
│       ├── core
│       │   ├── __tests__
│       │   │   └── context-manager.test.js
│       │   ├── context-manager.js
│       │   ├── direct-functions
│       │   │   ├── add-dependency.js
│       │   │   ├── add-subtask.js
│       │   │   ├── add-tag.js
│       │   │   ├── add-task.js
│       │   │   ├── analyze-task-complexity.js
│       │   │   ├── cache-stats.js
│       │   │   ├── clear-subtasks.js
│       │   │   ├── complexity-report.js
│       │   │   ├── copy-tag.js
│       │   │   ├── create-tag-from-branch.js
│       │   │   ├── delete-tag.js
│       │   │   ├── expand-all-tasks.js
│       │   │   ├── expand-task.js
│       │   │   ├── fix-dependencies.js
│       │   │   ├── generate-task-files.js
│       │   │   ├── initialize-project.js
│       │   │   ├── list-tags.js
│       │   │   ├── models.js
│       │   │   ├── move-task-cross-tag.js
│       │   │   ├── move-task.js
│       │   │   ├── next-task.js
│       │   │   ├── parse-prd.js
│       │   │   ├── remove-dependency.js
│       │   │   ├── remove-subtask.js
│       │   │   ├── remove-task.js
│       │   │   ├── rename-tag.js
│       │   │   ├── research.js
│       │   │   ├── response-language.js
│       │   │   ├── rules.js
│       │   │   ├── scope-down.js
│       │   │   ├── scope-up.js
│       │   │   ├── set-task-status.js
│       │   │   ├── update-subtask-by-id.js
│       │   │   ├── update-task-by-id.js
│       │   │   ├── update-tasks.js
│       │   │   ├── use-tag.js
│       │   │   └── validate-dependencies.js
│       │   ├── task-master-core.js
│       │   └── utils
│       │       ├── env-utils.js
│       │       └── path-utils.js
│       ├── custom-sdk
│       │   ├── errors.js
│       │   ├── index.js
│       │   ├── json-extractor.js
│       │   ├── language-model.js
│       │   ├── message-converter.js
│       │   └── schema-converter.js
│       ├── index.js
│       ├── logger.js
│       ├── providers
│       │   └── mcp-provider.js
│       └── tools
│           ├── add-dependency.js
│           ├── add-subtask.js
│           ├── add-tag.js
│           ├── add-task.js
│           ├── analyze.js
│           ├── clear-subtasks.js
│           ├── complexity-report.js
│           ├── copy-tag.js
│           ├── delete-tag.js
│           ├── expand-all.js
│           ├── expand-task.js
│           ├── fix-dependencies.js
│           ├── generate.js
│           ├── get-operation-status.js
│           ├── index.js
│           ├── initialize-project.js
│           ├── list-tags.js
│           ├── models.js
│           ├── move-task.js
│           ├── next-task.js
│           ├── parse-prd.js
│           ├── README-ZOD-V3.md
│           ├── remove-dependency.js
│           ├── remove-subtask.js
│           ├── remove-task.js
│           ├── rename-tag.js
│           ├── research.js
│           ├── response-language.js
│           ├── rules.js
│           ├── scope-down.js
│           ├── scope-up.js
│           ├── set-task-status.js
│           ├── tool-registry.js
│           ├── update-subtask.js
│           ├── update-task.js
│           ├── update.js
│           ├── use-tag.js
│           ├── utils.js
│           └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│   ├── ai-sdk-provider-grok-cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── errors.test.ts
│   │   │   ├── errors.ts
│   │   │   ├── grok-cli-language-model.ts
│   │   │   ├── grok-cli-provider.test.ts
│   │   │   ├── grok-cli-provider.ts
│   │   │   ├── index.ts
│   │   │   ├── json-extractor.test.ts
│   │   │   ├── json-extractor.ts
│   │   │   ├── message-converter.test.ts
│   │   │   ├── message-converter.ts
│   │   │   └── types.ts
│   │   └── tsconfig.json
│   ├── build-config
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   └── tsdown.base.ts
│   │   └── tsconfig.json
│   ├── claude-code-plugin
│   │   ├── .claude-plugin
│   │   │   └── plugin.json
│   │   ├── .gitignore
│   │   ├── agents
│   │   │   ├── task-checker.md
│   │   │   ├── task-executor.md
│   │   │   └── task-orchestrator.md
│   │   ├── CHANGELOG.md
│   │   ├── commands
│   │   │   ├── add-dependency.md
│   │   │   ├── add-subtask.md
│   │   │   ├── add-task.md
│   │   │   ├── analyze-complexity.md
│   │   │   ├── analyze-project.md
│   │   │   ├── auto-implement-tasks.md
│   │   │   ├── command-pipeline.md
│   │   │   ├── complexity-report.md
│   │   │   ├── convert-task-to-subtask.md
│   │   │   ├── expand-all-tasks.md
│   │   │   ├── expand-task.md
│   │   │   ├── fix-dependencies.md
│   │   │   ├── generate-tasks.md
│   │   │   ├── help.md
│   │   │   ├── init-project-quick.md
│   │   │   ├── init-project.md
│   │   │   ├── install-taskmaster.md
│   │   │   ├── learn.md
│   │   │   ├── list-tasks-by-status.md
│   │   │   ├── list-tasks-with-subtasks.md
│   │   │   ├── list-tasks.md
│   │   │   ├── next-task.md
│   │   │   ├── parse-prd-with-research.md
│   │   │   ├── parse-prd.md
│   │   │   ├── project-status.md
│   │   │   ├── quick-install-taskmaster.md
│   │   │   ├── remove-all-subtasks.md
│   │   │   ├── remove-dependency.md
│   │   │   ├── remove-subtask.md
│   │   │   ├── remove-subtasks.md
│   │   │   ├── remove-task.md
│   │   │   ├── setup-models.md
│   │   │   ├── show-task.md
│   │   │   ├── smart-workflow.md
│   │   │   ├── sync-readme.md
│   │   │   ├── tm-main.md
│   │   │   ├── to-cancelled.md
│   │   │   ├── to-deferred.md
│   │   │   ├── to-done.md
│   │   │   ├── to-in-progress.md
│   │   │   ├── to-pending.md
│   │   │   ├── to-review.md
│   │   │   ├── update-single-task.md
│   │   │   ├── update-task.md
│   │   │   ├── update-tasks-from-id.md
│   │   │   ├── validate-dependencies.md
│   │   │   └── view-models.md
│   │   ├── mcp.json
│   │   └── package.json
│   ├── tm-bridge
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── add-tag-bridge.ts
│   │   │   ├── bridge-types.ts
│   │   │   ├── bridge-utils.ts
│   │   │   ├── expand-bridge.ts
│   │   │   ├── index.ts
│   │   │   ├── tags-bridge.ts
│   │   │   ├── update-bridge.ts
│   │   │   └── use-tag-bridge.ts
│   │   └── tsconfig.json
│   └── tm-core
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── docs
│       │   └── listTasks-architecture.md
│       ├── package.json
│       ├── POC-STATUS.md
│       ├── README.md
│       ├── src
│       │   ├── common
│       │   │   ├── constants
│       │   │   │   ├── index.ts
│       │   │   │   ├── paths.ts
│       │   │   │   └── providers.ts
│       │   │   ├── errors
│       │   │   │   ├── index.ts
│       │   │   │   └── task-master-error.ts
│       │   │   ├── interfaces
│       │   │   │   ├── configuration.interface.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── storage.interface.ts
│       │   │   ├── logger
│       │   │   │   ├── factory.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── logger.spec.ts
│       │   │   │   └── logger.ts
│       │   │   ├── mappers
│       │   │   │   ├── TaskMapper.test.ts
│       │   │   │   └── TaskMapper.ts
│       │   │   ├── types
│       │   │   │   ├── database.types.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── legacy.ts
│       │   │   │   └── repository-types.ts
│       │   │   └── utils
│       │   │       ├── git-utils.ts
│       │   │       ├── id-generator.ts
│       │   │       ├── index.ts
│       │   │       ├── path-helpers.ts
│       │   │       ├── path-normalizer.spec.ts
│       │   │       ├── path-normalizer.ts
│       │   │       ├── project-root-finder.spec.ts
│       │   │       ├── project-root-finder.ts
│       │   │       ├── run-id-generator.spec.ts
│       │   │       └── run-id-generator.ts
│       │   ├── index.ts
│       │   ├── modules
│       │   │   ├── ai
│       │   │   │   ├── index.ts
│       │   │   │   ├── interfaces
│       │   │   │   │   └── ai-provider.interface.ts
│       │   │   │   └── providers
│       │   │   │       ├── base-provider.ts
│       │   │   │       └── index.ts
│       │   │   ├── auth
│       │   │   │   ├── auth-domain.spec.ts
│       │   │   │   ├── auth-domain.ts
│       │   │   │   ├── config.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── managers
│       │   │   │   │   ├── auth-manager.spec.ts
│       │   │   │   │   └── auth-manager.ts
│       │   │   │   ├── services
│       │   │   │   │   ├── context-store.ts
│       │   │   │   │   ├── oauth-service.ts
│       │   │   │   │   ├── organization.service.ts
│       │   │   │   │   ├── supabase-session-storage.spec.ts
│       │   │   │   │   └── supabase-session-storage.ts
│       │   │   │   └── types.ts
│       │   │   ├── briefs
│       │   │   │   ├── briefs-domain.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── services
│       │   │   │   │   └── brief-service.ts
│       │   │   │   ├── types.ts
│       │   │   │   └── utils
│       │   │   │       └── url-parser.ts
│       │   │   ├── commands
│       │   │   │   └── index.ts
│       │   │   ├── config
│       │   │   │   ├── config-domain.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── managers
│       │   │   │   │   ├── config-manager.spec.ts
│       │   │   │   │   └── config-manager.ts
│       │   │   │   └── services
│       │   │   │       ├── config-loader.service.spec.ts
│       │   │   │       ├── config-loader.service.ts
│       │   │   │       ├── config-merger.service.spec.ts
│       │   │   │       ├── config-merger.service.ts
│       │   │   │       ├── config-persistence.service.spec.ts
│       │   │   │       ├── config-persistence.service.ts
│       │   │   │       ├── environment-config-provider.service.spec.ts
│       │   │   │       ├── environment-config-provider.service.ts
│       │   │   │       ├── index.ts
│       │   │   │       ├── runtime-state-manager.service.spec.ts
│       │   │   │       └── runtime-state-manager.service.ts
│       │   │   ├── dependencies
│       │   │   │   └── index.ts
│       │   │   ├── execution
│       │   │   │   ├── executors
│       │   │   │   │   ├── base-executor.ts
│       │   │   │   │   ├── claude-executor.ts
│       │   │   │   │   └── executor-factory.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── services
│       │   │   │   │   └── executor-service.ts
│       │   │   │   └── types.ts
│       │   │   ├── git
│       │   │   │   ├── adapters
│       │   │   │   │   ├── git-adapter.test.ts
│       │   │   │   │   └── git-adapter.ts
│       │   │   │   ├── git-domain.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── services
│       │   │   │       ├── branch-name-generator.spec.ts
│       │   │   │       ├── branch-name-generator.ts
│       │   │   │       ├── commit-message-generator.test.ts
│       │   │   │       ├── commit-message-generator.ts
│       │   │   │       ├── scope-detector.test.ts
│       │   │   │       ├── scope-detector.ts
│       │   │   │       ├── template-engine.test.ts
│       │   │   │       └── template-engine.ts
│       │   │   ├── integration
│       │   │   │   ├── clients
│       │   │   │   │   ├── index.ts
│       │   │   │   │   └── supabase-client.ts
│       │   │   │   ├── integration-domain.ts
│       │   │   │   └── services
│       │   │   │       ├── export.service.ts
│       │   │   │       ├── task-expansion.service.ts
│       │   │   │       └── task-retrieval.service.ts
│       │   │   ├── reports
│       │   │   │   ├── index.ts
│       │   │   │   ├── managers
│       │   │   │   │   └── complexity-report-manager.ts
│       │   │   │   └── types.ts
│       │   │   ├── storage
│       │   │   │   ├── adapters
│       │   │   │   │   ├── activity-logger.ts
│       │   │   │   │   ├── api-storage.ts
│       │   │   │   │   └── file-storage
│       │   │   │   │       ├── file-operations.ts
│       │   │   │   │       ├── file-storage.ts
│       │   │   │   │       ├── format-handler.ts
│       │   │   │   │       ├── index.ts
│       │   │   │   │       └── path-resolver.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── services
│       │   │   │   │   └── storage-factory.ts
│       │   │   │   └── utils
│       │   │   │       └── api-client.ts
│       │   │   ├── tasks
│       │   │   │   ├── entities
│       │   │   │   │   └── task.entity.ts
│       │   │   │   ├── parser
│       │   │   │   │   └── index.ts
│       │   │   │   ├── repositories
│       │   │   │   │   ├── supabase
│       │   │   │   │   │   ├── dependency-fetcher.ts
│       │   │   │   │   │   ├── index.ts
│       │   │   │   │   │   └── supabase-repository.ts
│       │   │   │   │   └── task-repository.interface.ts
│       │   │   │   ├── services
│       │   │   │   │   ├── preflight-checker.service.ts
│       │   │   │   │   ├── tag.service.ts
│       │   │   │   │   ├── task-execution-service.ts
│       │   │   │   │   ├── task-loader.service.ts
│       │   │   │   │   └── task-service.ts
│       │   │   │   └── tasks-domain.ts
│       │   │   ├── ui
│       │   │   │   └── index.ts
│       │   │   └── workflow
│       │   │       ├── managers
│       │   │       │   ├── workflow-state-manager.spec.ts
│       │   │       │   └── workflow-state-manager.ts
│       │   │       ├── orchestrators
│       │   │       │   ├── workflow-orchestrator.test.ts
│       │   │       │   └── workflow-orchestrator.ts
│       │   │       ├── services
│       │   │       │   ├── test-result-validator.test.ts
│       │   │       │   ├── test-result-validator.ts
│       │   │       │   ├── test-result-validator.types.ts
│       │   │       │   ├── workflow-activity-logger.ts
│       │   │       │   └── workflow.service.ts
│       │   │       ├── types.ts
│       │   │       └── workflow-domain.ts
│       │   ├── subpath-exports.test.ts
│       │   ├── tm-core.ts
│       │   └── utils
│       │       └── time.utils.ts
│       ├── tests
│       │   ├── auth
│       │   │   └── auth-refresh.test.ts
│       │   ├── integration
│       │   │   ├── auth-token-refresh.test.ts
│       │   │   ├── list-tasks.test.ts
│       │   │   └── storage
│       │   │       └── activity-logger.test.ts
│       │   ├── mocks
│       │   │   └── mock-provider.ts
│       │   ├── setup.ts
│       │   └── unit
│       │       ├── base-provider.test.ts
│       │       ├── executor.test.ts
│       │       └── smoke.test.ts
│       ├── tsconfig.json
│       └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│   ├── create-worktree.sh
│   ├── dev.js
│   ├── init.js
│   ├── list-worktrees.sh
│   ├── modules
│   │   ├── ai-services-unified.js
│   │   ├── bridge-utils.js
│   │   ├── commands.js
│   │   ├── config-manager.js
│   │   ├── dependency-manager.js
│   │   ├── index.js
│   │   ├── prompt-manager.js
│   │   ├── supported-models.json
│   │   ├── sync-readme.js
│   │   ├── task-manager
│   │   │   ├── add-subtask.js
│   │   │   ├── add-task.js
│   │   │   ├── analyze-task-complexity.js
│   │   │   ├── clear-subtasks.js
│   │   │   ├── expand-all-tasks.js
│   │   │   ├── expand-task.js
│   │   │   ├── find-next-task.js
│   │   │   ├── generate-task-files.js
│   │   │   ├── is-task-dependent.js
│   │   │   ├── list-tasks.js
│   │   │   ├── migrate.js
│   │   │   ├── models.js
│   │   │   ├── move-task.js
│   │   │   ├── parse-prd
│   │   │   │   ├── index.js
│   │   │   │   ├── parse-prd-config.js
│   │   │   │   ├── parse-prd-helpers.js
│   │   │   │   ├── parse-prd-non-streaming.js
│   │   │   │   ├── parse-prd-streaming.js
│   │   │   │   └── parse-prd.js
│   │   │   ├── remove-subtask.js
│   │   │   ├── remove-task.js
│   │   │   ├── research.js
│   │   │   ├── response-language.js
│   │   │   ├── scope-adjustment.js
│   │   │   ├── set-task-status.js
│   │   │   ├── tag-management.js
│   │   │   ├── task-exists.js
│   │   │   ├── update-single-task-status.js
│   │   │   ├── update-subtask-by-id.js
│   │   │   ├── update-task-by-id.js
│   │   │   └── update-tasks.js
│   │   ├── task-manager.js
│   │   ├── ui.js
│   │   ├── update-config-tokens.js
│   │   ├── utils
│   │   │   ├── contextGatherer.js
│   │   │   ├── fuzzyTaskSearch.js
│   │   │   └── git-utils.js
│   │   └── utils.js
│   ├── task-complexity-report.json
│   ├── test-claude-errors.js
│   └── test-claude.js
├── sonar-project.properties
├── src
│   ├── ai-providers
│   │   ├── anthropic.js
│   │   ├── azure.js
│   │   ├── base-provider.js
│   │   ├── bedrock.js
│   │   ├── claude-code.js
│   │   ├── codex-cli.js
│   │   ├── gemini-cli.js
│   │   ├── google-vertex.js
│   │   ├── google.js
│   │   ├── grok-cli.js
│   │   ├── groq.js
│   │   ├── index.js
│   │   ├── lmstudio.js
│   │   ├── ollama.js
│   │   ├── openai-compatible.js
│   │   ├── openai.js
│   │   ├── openrouter.js
│   │   ├── perplexity.js
│   │   ├── xai.js
│   │   ├── zai-coding.js
│   │   └── zai.js
│   ├── constants
│   │   ├── commands.js
│   │   ├── paths.js
│   │   ├── profiles.js
│   │   ├── rules-actions.js
│   │   ├── task-priority.js
│   │   └── task-status.js
│   ├── profiles
│   │   ├── amp.js
│   │   ├── base-profile.js
│   │   ├── claude.js
│   │   ├── cline.js
│   │   ├── codex.js
│   │   ├── cursor.js
│   │   ├── gemini.js
│   │   ├── index.js
│   │   ├── kilo.js
│   │   ├── kiro.js
│   │   ├── opencode.js
│   │   ├── roo.js
│   │   ├── trae.js
│   │   ├── vscode.js
│   │   ├── windsurf.js
│   │   └── zed.js
│   ├── progress
│   │   ├── base-progress-tracker.js
│   │   ├── cli-progress-factory.js
│   │   ├── parse-prd-tracker.js
│   │   ├── progress-tracker-builder.js
│   │   └── tracker-ui.js
│   ├── prompts
│   │   ├── add-task.json
│   │   ├── analyze-complexity.json
│   │   ├── expand-task.json
│   │   ├── parse-prd.json
│   │   ├── README.md
│   │   ├── research.json
│   │   ├── schemas
│   │   │   ├── parameter.schema.json
│   │   │   ├── prompt-template.schema.json
│   │   │   ├── README.md
│   │   │   └── variant.schema.json
│   │   ├── update-subtask.json
│   │   ├── update-task.json
│   │   └── update-tasks.json
│   ├── provider-registry
│   │   └── index.js
│   ├── schemas
│   │   ├── add-task.js
│   │   ├── analyze-complexity.js
│   │   ├── base-schemas.js
│   │   ├── expand-task.js
│   │   ├── parse-prd.js
│   │   ├── registry.js
│   │   ├── update-subtask.js
│   │   ├── update-task.js
│   │   └── update-tasks.js
│   ├── task-master.js
│   ├── ui
│   │   ├── confirm.js
│   │   ├── indicators.js
│   │   └── parse-prd.js
│   └── utils
│       ├── asset-resolver.js
│       ├── create-mcp-config.js
│       ├── format.js
│       ├── getVersion.js
│       ├── logger-utils.js
│       ├── manage-gitignore.js
│       ├── path-utils.js
│       ├── profiles.js
│       ├── rule-transformer.js
│       ├── stream-parser.js
│       └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│   ├── e2e
│   │   ├── e2e_helpers.sh
│   │   ├── parse_llm_output.cjs
│   │   ├── run_e2e.sh
│   │   ├── run_fallback_verification.sh
│   │   └── test_llm_analysis.sh
│   ├── fixtures
│   │   ├── .taskmasterconfig
│   │   ├── sample-claude-response.js
│   │   ├── sample-prd.txt
│   │   └── sample-tasks.js
│   ├── helpers
│   │   └── tool-counts.js
│   ├── integration
│   │   ├── claude-code-error-handling.test.js
│   │   ├── claude-code-optional.test.js
│   │   ├── cli
│   │   │   ├── commands.test.js
│   │   │   ├── complex-cross-tag-scenarios.test.js
│   │   │   └── move-cross-tag.test.js
│   │   ├── manage-gitignore.test.js
│   │   ├── mcp-server
│   │   │   └── direct-functions.test.js
│   │   ├── move-task-cross-tag.integration.test.js
│   │   ├── move-task-simple.integration.test.js
│   │   ├── profiles
│   │   │   ├── amp-init-functionality.test.js
│   │   │   ├── claude-init-functionality.test.js
│   │   │   ├── cline-init-functionality.test.js
│   │   │   ├── codex-init-functionality.test.js
│   │   │   ├── cursor-init-functionality.test.js
│   │   │   ├── gemini-init-functionality.test.js
│   │   │   ├── opencode-init-functionality.test.js
│   │   │   ├── roo-files-inclusion.test.js
│   │   │   ├── roo-init-functionality.test.js
│   │   │   ├── rules-files-inclusion.test.js
│   │   │   ├── trae-init-functionality.test.js
│   │   │   ├── vscode-init-functionality.test.js
│   │   │   └── windsurf-init-functionality.test.js
│   │   └── providers
│   │       └── temperature-support.test.js
│   ├── manual
│   │   ├── progress
│   │   │   ├── parse-prd-analysis.js
│   │   │   ├── test-parse-prd.js
│   │   │   └── TESTING_GUIDE.md
│   │   └── prompts
│   │       ├── prompt-test.js
│   │       └── README.md
│   ├── README.md
│   ├── setup.js
│   └── unit
│       ├── ai-providers
│       │   ├── base-provider.test.js
│       │   ├── claude-code.test.js
│       │   ├── codex-cli.test.js
│       │   ├── gemini-cli.test.js
│       │   ├── lmstudio.test.js
│       │   ├── mcp-components.test.js
│       │   ├── openai-compatible.test.js
│       │   ├── openai.test.js
│       │   ├── provider-registry.test.js
│       │   ├── zai-coding.test.js
│       │   ├── zai-provider.test.js
│       │   ├── zai-schema-introspection.test.js
│       │   └── zai.test.js
│       ├── ai-services-unified.test.js
│       ├── commands.test.js
│       ├── config-manager.test.js
│       ├── config-manager.test.mjs
│       ├── dependency-manager.test.js
│       ├── init.test.js
│       ├── initialize-project.test.js
│       ├── kebab-case-validation.test.js
│       ├── manage-gitignore.test.js
│       ├── mcp
│       │   └── tools
│       │       ├── __mocks__
│       │       │   └── move-task.js
│       │       ├── add-task.test.js
│       │       ├── analyze-complexity.test.js
│       │       ├── expand-all.test.js
│       │       ├── get-tasks.test.js
│       │       ├── initialize-project.test.js
│       │       ├── move-task-cross-tag-options.test.js
│       │       ├── move-task-cross-tag.test.js
│       │       ├── remove-task.test.js
│       │       └── tool-registration.test.js
│       ├── mcp-providers
│       │   ├── mcp-components.test.js
│       │   └── mcp-provider.test.js
│       ├── parse-prd.test.js
│       ├── profiles
│       │   ├── amp-integration.test.js
│       │   ├── claude-integration.test.js
│       │   ├── cline-integration.test.js
│       │   ├── codex-integration.test.js
│       │   ├── cursor-integration.test.js
│       │   ├── gemini-integration.test.js
│       │   ├── kilo-integration.test.js
│       │   ├── kiro-integration.test.js
│       │   ├── mcp-config-validation.test.js
│       │   ├── opencode-integration.test.js
│       │   ├── profile-safety-check.test.js
│       │   ├── roo-integration.test.js
│       │   ├── rule-transformer-cline.test.js
│       │   ├── rule-transformer-cursor.test.js
│       │   ├── rule-transformer-gemini.test.js
│       │   ├── rule-transformer-kilo.test.js
│       │   ├── rule-transformer-kiro.test.js
│       │   ├── rule-transformer-opencode.test.js
│       │   ├── rule-transformer-roo.test.js
│       │   ├── rule-transformer-trae.test.js
│       │   ├── rule-transformer-vscode.test.js
│       │   ├── rule-transformer-windsurf.test.js
│       │   ├── rule-transformer-zed.test.js
│       │   ├── rule-transformer.test.js
│       │   ├── selective-profile-removal.test.js
│       │   ├── subdirectory-support.test.js
│       │   ├── trae-integration.test.js
│       │   ├── vscode-integration.test.js
│       │   ├── windsurf-integration.test.js
│       │   └── zed-integration.test.js
│       ├── progress
│       │   └── base-progress-tracker.test.js
│       ├── prompt-manager.test.js
│       ├── prompts
│       │   ├── expand-task-prompt.test.js
│       │   └── prompt-migration.test.js
│       ├── scripts
│       │   └── modules
│       │       ├── commands
│       │       │   ├── move-cross-tag.test.js
│       │       │   └── README.md
│       │       ├── dependency-manager
│       │       │   ├── circular-dependencies.test.js
│       │       │   ├── cross-tag-dependencies.test.js
│       │       │   └── fix-dependencies-command.test.js
│       │       ├── task-manager
│       │       │   ├── add-subtask.test.js
│       │       │   ├── add-task.test.js
│       │       │   ├── analyze-task-complexity.test.js
│       │       │   ├── clear-subtasks.test.js
│       │       │   ├── complexity-report-tag-isolation.test.js
│       │       │   ├── expand-all-tasks.test.js
│       │       │   ├── expand-task.test.js
│       │       │   ├── find-next-task.test.js
│       │       │   ├── generate-task-files.test.js
│       │       │   ├── list-tasks.test.js
│       │       │   ├── models-baseurl.test.js
│       │       │   ├── move-task-cross-tag.test.js
│       │       │   ├── move-task.test.js
│       │       │   ├── parse-prd-schema.test.js
│       │       │   ├── parse-prd.test.js
│       │       │   ├── remove-subtask.test.js
│       │       │   ├── remove-task.test.js
│       │       │   ├── research.test.js
│       │       │   ├── scope-adjustment.test.js
│       │       │   ├── set-task-status.test.js
│       │       │   ├── setup.js
│       │       │   ├── update-single-task-status.test.js
│       │       │   ├── update-subtask-by-id.test.js
│       │       │   ├── update-task-by-id.test.js
│       │       │   └── update-tasks.test.js
│       │       ├── ui
│       │       │   └── cross-tag-error-display.test.js
│       │       └── utils-tag-aware-paths.test.js
│       ├── task-finder.test.js
│       ├── task-manager
│       │   ├── clear-subtasks.test.js
│       │   ├── move-task.test.js
│       │   ├── tag-boundary.test.js
│       │   └── tag-management.test.js
│       ├── task-master.test.js
│       ├── ui
│       │   └── indicators.test.js
│       ├── ui.test.js
│       ├── utils-strip-ansi.test.js
│       └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```

# Files

--------------------------------------------------------------------------------
/scripts/modules/task-manager/analyze-task-complexity.js:
--------------------------------------------------------------------------------

```javascript
  1 | import chalk from 'chalk';
  2 | import boxen from 'boxen';
  3 | import readline from 'readline';
  4 | import fs from 'fs';
  5 | 
  6 | import { log, readJSON, isSilentMode } from '../utils.js';
  7 | 
  8 | import {
  9 | 	startLoadingIndicator,
 10 | 	stopLoadingIndicator,
 11 | 	displayAiUsageSummary
 12 | } from '../ui.js';
 13 | 
 14 | import { generateObjectService } from '../ai-services-unified.js';
 15 | import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
 16 | 
 17 | import {
 18 | 	getDebugFlag,
 19 | 	getProjectName,
 20 | 	hasCodebaseAnalysis
 21 | } from '../config-manager.js';
 22 | import { getPromptManager } from '../prompt-manager.js';
 23 | import { LEGACY_TASKS_FILE } from '../../../src/constants/paths.js';
 24 | import { resolveComplexityReportOutputPath } from '../../../src/utils/path-utils.js';
 25 | import { ContextGatherer } from '../utils/contextGatherer.js';
 26 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
 27 | import { flattenTasksWithSubtasks } from '../utils.js';
 28 | 
 29 | /**
 30 |  * Analyzes task complexity and generates expansion recommendations
 31 |  * @param {Object} options Command options
 32 |  * @param {string} options.file - Path to tasks file
 33 |  * @param {string} options.output - Path to report output file
 34 |  * @param {string|number} [options.threshold] - Complexity threshold
 35 |  * @param {boolean} [options.research] - Use research role
 36 |  * @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
 37 |  * @param {string} [options.tag] - Tag for the task
 38 |  * @param {string} [options.id] - Comma-separated list of task IDs to analyze specifically
 39 |  * @param {number} [options.from] - Starting task ID in a range to analyze
 40 |  * @param {number} [options.to] - Ending task ID in a range to analyze
 41 |  * @param {Object} [options._filteredTasksData] - Pre-filtered task data (internal use)
 42 |  * @param {number} [options._originalTaskCount] - Original task count (internal use)
 43 |  * @param {Object} context - Context object, potentially containing session and mcpLog
 44 |  * @param {Object} [context.session] - Session object from MCP server (optional)
 45 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
 46 |  * @param {function} [context.reportProgress] - Deprecated: Function to report progress (ignored)
 47 |  */
 48 | async function analyzeTaskComplexity(options, context = {}) {
 49 | 	const { session, mcpLog } = context;
 50 | 	const tasksPath = options.file || LEGACY_TASKS_FILE;
 51 | 	const thresholdScore = parseFloat(options.threshold || '5');
 52 | 	const useResearch = options.research || false;
 53 | 	const projectRoot = options.projectRoot;
 54 | 	const tag = options.tag;
 55 | 	// New parameters for task ID filtering
 56 | 	const specificIds = options.id
 57 | 		? options.id
 58 | 				.split(',')
 59 | 				.map((id) => parseInt(id.trim(), 10))
 60 | 				.filter((id) => !Number.isNaN(id))
 61 | 		: null;
 62 | 	const fromId = options.from !== undefined ? parseInt(options.from, 10) : null;
 63 | 	const toId = options.to !== undefined ? parseInt(options.to, 10) : null;
 64 | 
 65 | 	const outputFormat = mcpLog ? 'json' : 'text';
 66 | 
 67 | 	const reportLog = (message, level = 'info') => {
 68 | 		if (mcpLog) {
 69 | 			mcpLog[level](message);
 70 | 		} else if (!isSilentMode() && outputFormat === 'text') {
 71 | 			log(level, message);
 72 | 		}
 73 | 	};
 74 | 
 75 | 	// Resolve output path using tag-aware resolution
 76 | 	const outputPath = resolveComplexityReportOutputPath(
 77 | 		options.output,
 78 | 		{ projectRoot, tag },
 79 | 		reportLog
 80 | 	);
 81 | 
 82 | 	if (outputFormat === 'text') {
 83 | 		console.log(
 84 | 			chalk.blue(
 85 | 				'Analyzing task complexity and generating expansion recommendations...'
 86 | 			)
 87 | 		);
 88 | 	}
 89 | 
 90 | 	try {
 91 | 		reportLog(`Reading tasks from ${tasksPath}...`, 'info');
 92 | 		let tasksData;
 93 | 		let originalTaskCount = 0;
 94 | 		let originalData = null;
 95 | 
 96 | 		if (options._filteredTasksData) {
 97 | 			tasksData = options._filteredTasksData;
 98 | 			originalTaskCount = options._originalTaskCount || tasksData.tasks.length;
 99 | 			if (!options._originalTaskCount) {
100 | 				try {
101 | 					originalData = readJSON(tasksPath, projectRoot, tag);
102 | 					if (originalData && originalData.tasks) {
103 | 						originalTaskCount = originalData.tasks.length;
104 | 					}
105 | 				} catch (e) {
106 | 					log('warn', `Could not read original tasks file: ${e.message}`);
107 | 				}
108 | 			}
109 | 		} else {
110 | 			originalData = readJSON(tasksPath, projectRoot, tag);
111 | 			if (
112 | 				!originalData ||
113 | 				!originalData.tasks ||
114 | 				!Array.isArray(originalData.tasks) ||
115 | 				originalData.tasks.length === 0
116 | 			) {
117 | 				throw new Error('No tasks found in the tasks file');
118 | 			}
119 | 			originalTaskCount = originalData.tasks.length;
120 | 
121 | 			// Filter tasks based on active status
122 | 			const activeStatuses = ['pending', 'blocked', 'in-progress'];
123 | 			let filteredTasks = originalData.tasks.filter((task) =>
124 | 				activeStatuses.includes(task.status?.toLowerCase() || 'pending')
125 | 			);
126 | 
127 | 			// Apply ID filtering if specified
128 | 			if (specificIds && specificIds.length > 0) {
129 | 				reportLog(
130 | 					`Filtering tasks by specific IDs: ${specificIds.join(', ')}`,
131 | 					'info'
132 | 				);
133 | 				filteredTasks = filteredTasks.filter((task) =>
134 | 					specificIds.includes(task.id)
135 | 				);
136 | 
137 | 				if (outputFormat === 'text') {
138 | 					if (filteredTasks.length === 0 && specificIds.length > 0) {
139 | 						console.log(
140 | 							chalk.yellow(
141 | 								`Warning: No active tasks found with IDs: ${specificIds.join(', ')}`
142 | 							)
143 | 						);
144 | 					} else if (filteredTasks.length < specificIds.length) {
145 | 						const foundIds = filteredTasks.map((t) => t.id);
146 | 						const missingIds = specificIds.filter(
147 | 							(id) => !foundIds.includes(id)
148 | 						);
149 | 						console.log(
150 | 							chalk.yellow(
151 | 								`Warning: Some requested task IDs were not found or are not active: ${missingIds.join(', ')}`
152 | 							)
153 | 						);
154 | 					}
155 | 				}
156 | 			}
157 | 			// Apply range filtering if specified
158 | 			else if (fromId !== null || toId !== null) {
159 | 				const effectiveFromId = fromId !== null ? fromId : 1;
160 | 				const effectiveToId =
161 | 					toId !== null
162 | 						? toId
163 | 						: Math.max(...originalData.tasks.map((t) => t.id));
164 | 
165 | 				reportLog(
166 | 					`Filtering tasks by ID range: ${effectiveFromId} to ${effectiveToId}`,
167 | 					'info'
168 | 				);
169 | 				filteredTasks = filteredTasks.filter(
170 | 					(task) => task.id >= effectiveFromId && task.id <= effectiveToId
171 | 				);
172 | 
173 | 				if (outputFormat === 'text' && filteredTasks.length === 0) {
174 | 					console.log(
175 | 						chalk.yellow(
176 | 							`Warning: No active tasks found in range: ${effectiveFromId}-${effectiveToId}`
177 | 						)
178 | 					);
179 | 				}
180 | 			}
181 | 
182 | 			tasksData = {
183 | 				...originalData,
184 | 				tasks: filteredTasks,
185 | 				_originalTaskCount: originalTaskCount
186 | 			};
187 | 		}
188 | 
189 | 		// --- Context Gathering ---
190 | 		let gatheredContext = '';
191 | 		if (originalData && originalData.tasks.length > 0) {
192 | 			try {
193 | 				const contextGatherer = new ContextGatherer(projectRoot, tag);
194 | 				const allTasksFlat = flattenTasksWithSubtasks(originalData.tasks);
195 | 				const fuzzySearch = new FuzzyTaskSearch(
196 | 					allTasksFlat,
197 | 					'analyze-complexity'
198 | 				);
199 | 				// Create a query from the tasks being analyzed
200 | 				const searchQuery = tasksData.tasks
201 | 					.map((t) => `${t.title} ${t.description}`)
202 | 					.join(' ');
203 | 				const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
204 | 					maxResults: 10
205 | 				});
206 | 				const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
207 | 
208 | 				if (relevantTaskIds.length > 0) {
209 | 					const contextResult = await contextGatherer.gather({
210 | 						tasks: relevantTaskIds,
211 | 						format: 'research'
212 | 					});
213 | 					gatheredContext = contextResult.context || '';
214 | 				}
215 | 			} catch (contextError) {
216 | 				reportLog(
217 | 					`Could not gather additional context: ${contextError.message}`,
218 | 					'warn'
219 | 				);
220 | 			}
221 | 		}
222 | 		// --- End Context Gathering ---
223 | 
224 | 		const skippedCount = originalTaskCount - tasksData.tasks.length;
225 | 		reportLog(
226 | 			`Found ${originalTaskCount} total tasks in the task file.`,
227 | 			'info'
228 | 		);
229 | 
230 | 		// Updated messaging to reflect filtering logic
231 | 		if (specificIds || fromId !== null || toId !== null) {
232 | 			const filterMsg = specificIds
233 | 				? `Analyzing ${tasksData.tasks.length} tasks with specific IDs: ${specificIds.join(', ')}`
234 | 				: `Analyzing ${tasksData.tasks.length} tasks in range: ${fromId || 1} to ${toId || 'end'}`;
235 | 
236 | 			reportLog(filterMsg, 'info');
237 | 			if (outputFormat === 'text') {
238 | 				console.log(chalk.blue(filterMsg));
239 | 			}
240 | 		} else if (skippedCount > 0) {
241 | 			const skipMessage = `Skipping ${skippedCount} tasks marked as done/cancelled/deferred. Analyzing ${tasksData.tasks.length} active tasks.`;
242 | 			reportLog(skipMessage, 'info');
243 | 			if (outputFormat === 'text') {
244 | 				console.log(chalk.yellow(skipMessage));
245 | 			}
246 | 		}
247 | 
248 | 		// Check for existing report before doing analysis
249 | 		let existingReport = null;
250 | 		const existingAnalysisMap = new Map(); // For quick lookups by task ID
251 | 		try {
252 | 			if (fs.existsSync(outputPath)) {
253 | 				existingReport = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
254 | 				reportLog(`Found existing complexity report at ${outputPath}`, 'info');
255 | 
256 | 				if (
257 | 					existingReport &&
258 | 					existingReport.complexityAnalysis &&
259 | 					Array.isArray(existingReport.complexityAnalysis)
260 | 				) {
261 | 					// Create lookup map of existing analysis entries
262 | 					existingReport.complexityAnalysis.forEach((item) => {
263 | 						existingAnalysisMap.set(item.taskId, item);
264 | 					});
265 | 					reportLog(
266 | 						`Existing report contains ${existingReport.complexityAnalysis.length} task analyses`,
267 | 						'info'
268 | 					);
269 | 				}
270 | 			}
271 | 		} catch (readError) {
272 | 			reportLog(
273 | 				`Warning: Could not read existing report: ${readError.message}`,
274 | 				'warn'
275 | 			);
276 | 			existingReport = null;
277 | 			existingAnalysisMap.clear();
278 | 		}
279 | 
280 | 		if (tasksData.tasks.length === 0) {
281 | 			// If using ID filtering but no matching tasks, return existing report or empty
282 | 			if (existingReport && (specificIds || fromId !== null || toId !== null)) {
283 | 				reportLog(
284 | 					'No matching tasks found for analysis. Keeping existing report.',
285 | 					'info'
286 | 				);
287 | 				if (outputFormat === 'text') {
288 | 					console.log(
289 | 						chalk.yellow(
290 | 							'No matching tasks found for analysis. Keeping existing report.'
291 | 						)
292 | 					);
293 | 				}
294 | 				return {
295 | 					report: existingReport,
296 | 					telemetryData: null
297 | 				};
298 | 			}
299 | 
300 | 			// Otherwise create empty report
301 | 			const emptyReport = {
302 | 				meta: {
303 | 					generatedAt: new Date().toISOString(),
304 | 					tasksAnalyzed: 0,
305 | 					thresholdScore: thresholdScore,
306 | 					projectName: getProjectName(session),
307 | 					usedResearch: useResearch
308 | 				},
309 | 				complexityAnalysis: existingReport?.complexityAnalysis || []
310 | 			};
311 | 			reportLog(`Writing complexity report to ${outputPath}...`, 'info');
312 | 			fs.writeFileSync(
313 | 				outputPath,
314 | 				JSON.stringify(emptyReport, null, '\t'),
315 | 				'utf8'
316 | 			);
317 | 			reportLog(
318 | 				`Task complexity analysis complete. Report written to ${outputPath}`,
319 | 				'success'
320 | 			);
321 | 			if (outputFormat === 'text') {
322 | 				console.log(
323 | 					chalk.green(
324 | 						`Task complexity analysis complete. Report written to ${outputPath}`
325 | 					)
326 | 				);
327 | 				const highComplexity = 0;
328 | 				const mediumComplexity = 0;
329 | 				const lowComplexity = 0;
330 | 				const totalAnalyzed = 0;
331 | 
332 | 				console.log('\nComplexity Analysis Summary:');
333 | 				console.log('----------------------------');
334 | 				console.log(`Tasks in input file: ${originalTaskCount}`);
335 | 				console.log(`Tasks successfully analyzed: ${totalAnalyzed}`);
336 | 				console.log(`High complexity tasks: ${highComplexity}`);
337 | 				console.log(`Medium complexity tasks: ${mediumComplexity}`);
338 | 				console.log(`Low complexity tasks: ${lowComplexity}`);
339 | 				console.log(
340 | 					`Sum verification: ${highComplexity + mediumComplexity + lowComplexity} (should equal ${totalAnalyzed})`
341 | 				);
342 | 				console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`);
343 | 				console.log(
344 | 					`\nSee ${outputPath} for the full report and expansion commands.`
345 | 				);
346 | 
347 | 				console.log(
348 | 					boxen(
349 | 						chalk.white.bold('Suggested Next Steps:') +
350 | 							'\n\n' +
351 | 							`${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` +
352 | 							`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` +
353 | 							`${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`,
354 | 						{
355 | 							padding: 1,
356 | 							borderColor: 'cyan',
357 | 							borderStyle: 'round',
358 | 							margin: { top: 1 }
359 | 						}
360 | 					)
361 | 				);
362 | 			}
363 | 			return {
364 | 				report: emptyReport,
365 | 				telemetryData: null
366 | 			};
367 | 		}
368 | 
369 | 		// Continue with regular analysis path
370 | 		// Load prompts using PromptManager
371 | 		const promptManager = getPromptManager();
372 | 
373 | 		// Check if Claude Code is being used as the provider
374 | 
375 | 		const promptParams = {
376 | 			tasks: tasksData.tasks,
377 | 			gatheredContext: gatheredContext || '',
378 | 			useResearch: useResearch,
379 | 			hasCodebaseAnalysis: hasCodebaseAnalysis(
380 | 				useResearch,
381 | 				projectRoot,
382 | 				session
383 | 			),
384 | 			projectRoot: projectRoot || ''
385 | 		};
386 | 
387 | 		const { systemPrompt, userPrompt: prompt } = await promptManager.loadPrompt(
388 | 			'analyze-complexity',
389 | 			promptParams,
390 | 			'default'
391 | 		);
392 | 
393 | 		let loadingIndicator = null;
394 | 		if (outputFormat === 'text') {
395 | 			loadingIndicator = startLoadingIndicator(
396 | 				`${useResearch ? 'Researching' : 'Analyzing'} the complexity of your tasks with AI...\n`
397 | 			);
398 | 		}
399 | 
400 | 		let aiServiceResponse = null;
401 | 		let complexityAnalysis = null;
402 | 
403 | 		try {
404 | 			const role = useResearch ? 'research' : 'main';
405 | 
406 | 			aiServiceResponse = await generateObjectService({
407 | 				prompt,
408 | 				systemPrompt,
409 | 				role,
410 | 				session,
411 | 				projectRoot,
412 | 				schema: COMMAND_SCHEMAS['analyze-complexity'],
413 | 				objectName: 'complexityAnalysis',
414 | 				commandName: 'analyze-complexity',
415 | 				outputType: mcpLog ? 'mcp' : 'cli'
416 | 			});
417 | 
418 | 			if (loadingIndicator) {
419 | 				stopLoadingIndicator(loadingIndicator);
420 | 				loadingIndicator = null;
421 | 			}
422 | 			if (outputFormat === 'text') {
423 | 				readline.clearLine(process.stdout, 0);
424 | 				readline.cursorTo(process.stdout, 0);
425 | 				console.log(chalk.green('AI service call complete.'));
426 | 			}
427 | 
428 | 			// With generateObject, we get structured data directly
429 | 			complexityAnalysis = aiServiceResponse.mainResult?.complexityAnalysis;
430 | 			reportLog(
431 | 				`Received ${complexityAnalysis.length} complexity analyses from AI.`,
432 | 				'info'
433 | 			);
434 | 
435 | 			const taskIds = tasksData.tasks.map((t) => t.id);
436 | 			const analysisTaskIds = complexityAnalysis.map((a) => a.taskId);
437 | 			const missingTaskIds = taskIds.filter(
438 | 				(id) => !analysisTaskIds.includes(id)
439 | 			);
440 | 
441 | 			if (missingTaskIds.length > 0) {
442 | 				reportLog(
443 | 					`Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}`,
444 | 					'warn'
445 | 				);
446 | 				if (outputFormat === 'text') {
447 | 					console.log(
448 | 						chalk.yellow(
449 | 							`Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}`
450 | 						)
451 | 					);
452 | 				}
453 | 				for (const missingId of missingTaskIds) {
454 | 					const missingTask = tasksData.tasks.find((t) => t.id === missingId);
455 | 					if (missingTask) {
456 | 						reportLog(`Adding default analysis for task ${missingId}`, 'info');
457 | 						complexityAnalysis.push({
458 | 							taskId: missingId,
459 | 							taskTitle: missingTask.title,
460 | 							complexityScore: 5,
461 | 							recommendedSubtasks: 3,
462 | 							expansionPrompt: `Break down this task with a focus on ${missingTask.title.toLowerCase()}.`,
463 | 							reasoning:
464 | 								'Automatically added due to missing analysis in AI response.'
465 | 						});
466 | 					}
467 | 				}
468 | 			}
469 | 
470 | 			// Merge with existing report - only keep entries from the current tag
471 | 			let finalComplexityAnalysis = [];
472 | 
473 | 			if (existingReport && Array.isArray(existingReport.complexityAnalysis)) {
474 | 				// Create a map of task IDs that we just analyzed
475 | 				const analyzedTaskIds = new Set(
476 | 					complexityAnalysis.map((item) => item.taskId)
477 | 				);
478 | 
479 | 				// Keep existing entries that weren't in this analysis run AND belong to the current tag
480 | 				// We determine tag membership by checking if the task ID exists in the current tag's tasks
481 | 				const currentTagTaskIds = new Set(tasksData.tasks.map((t) => t.id));
482 | 				const existingEntriesNotAnalyzed =
483 | 					existingReport.complexityAnalysis.filter(
484 | 						(item) =>
485 | 							!analyzedTaskIds.has(item.taskId) &&
486 | 							currentTagTaskIds.has(item.taskId) // Only keep entries for tasks in current tag
487 | 					);
488 | 
489 | 				// Combine with new analysis
490 | 				finalComplexityAnalysis = [
491 | 					...existingEntriesNotAnalyzed,
492 | 					...complexityAnalysis
493 | 				];
494 | 
495 | 				reportLog(
496 | 					`Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries from current tag`,
497 | 					'info'
498 | 				);
499 | 			} else {
500 | 				// No existing report or invalid format, just use the new analysis
501 | 				finalComplexityAnalysis = complexityAnalysis;
502 | 			}
503 | 
504 | 			const report = {
505 | 				meta: {
506 | 					generatedAt: new Date().toISOString(),
507 | 					tasksAnalyzed: tasksData.tasks.length,
508 | 					totalTasks: originalTaskCount,
509 | 					analysisCount: finalComplexityAnalysis.length,
510 | 					thresholdScore: thresholdScore,
511 | 					projectName: getProjectName(session),
512 | 					usedResearch: useResearch
513 | 				},
514 | 				complexityAnalysis: finalComplexityAnalysis
515 | 			};
516 | 			reportLog(`Writing complexity report to ${outputPath}...`, 'info');
517 | 			fs.writeFileSync(outputPath, JSON.stringify(report, null, '\t'), 'utf8');
518 | 
519 | 			reportLog(
520 | 				`Task complexity analysis complete. Report written to ${outputPath}`,
521 | 				'success'
522 | 			);
523 | 
524 | 			if (outputFormat === 'text') {
525 | 				console.log(
526 | 					chalk.green(
527 | 						`Task complexity analysis complete. Report written to ${outputPath}`
528 | 					)
529 | 				);
530 | 				// Calculate statistics specifically for this analysis run
531 | 				const highComplexity = complexityAnalysis.filter(
532 | 					(t) => t.complexityScore >= 8
533 | 				).length;
534 | 				const mediumComplexity = complexityAnalysis.filter(
535 | 					(t) => t.complexityScore >= 5 && t.complexityScore < 8
536 | 				).length;
537 | 				const lowComplexity = complexityAnalysis.filter(
538 | 					(t) => t.complexityScore < 5
539 | 				).length;
540 | 				const totalAnalyzed = complexityAnalysis.length;
541 | 
542 | 				console.log('\nCurrent Analysis Summary:');
543 | 				console.log('----------------------------');
544 | 				console.log(`Tasks analyzed in this run: ${totalAnalyzed}`);
545 | 				console.log(`High complexity tasks: ${highComplexity}`);
546 | 				console.log(`Medium complexity tasks: ${mediumComplexity}`);
547 | 				console.log(`Low complexity tasks: ${lowComplexity}`);
548 | 
549 | 				if (existingReport) {
550 | 					console.log('\nUpdated Report Summary:');
551 | 					console.log('----------------------------');
552 | 					console.log(
553 | 						`Total analyses in report: ${finalComplexityAnalysis.length}`
554 | 					);
555 | 					console.log(
556 | 						`Analyses from previous runs: ${finalComplexityAnalysis.length - totalAnalyzed}`
557 | 					);
558 | 					console.log(`New/updated analyses: ${totalAnalyzed}`);
559 | 				}
560 | 
561 | 				console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`);
562 | 				console.log(
563 | 					`\nSee ${outputPath} for the full report and expansion commands.`
564 | 				);
565 | 
566 | 				console.log(
567 | 					boxen(
568 | 						chalk.white.bold('Suggested Next Steps:') +
569 | 							'\n\n' +
570 | 							`${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` +
571 | 							`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` +
572 | 							`${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`,
573 | 						{
574 | 							padding: 1,
575 | 							borderColor: 'cyan',
576 | 							borderStyle: 'round',
577 | 							margin: { top: 1 }
578 | 						}
579 | 					)
580 | 				);
581 | 
582 | 				if (getDebugFlag(session)) {
583 | 					console.debug(
584 | 						chalk.gray(
585 | 							`Final analysis object: ${JSON.stringify(report, null, 2)}`
586 | 						)
587 | 					);
588 | 				}
589 | 
590 | 				if (aiServiceResponse.telemetryData) {
591 | 					displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
592 | 				}
593 | 			}
594 | 
595 | 			return {
596 | 				report: report,
597 | 				telemetryData: aiServiceResponse?.telemetryData,
598 | 				tagInfo: aiServiceResponse?.tagInfo
599 | 			};
600 | 		} catch (aiError) {
601 | 			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
602 | 			reportLog(`Error during AI service call: ${aiError.message}`, 'error');
603 | 			if (outputFormat === 'text') {
604 | 				console.error(
605 | 					chalk.red(`Error during AI service call: ${aiError.message}`)
606 | 				);
607 | 				if (aiError.message.includes('API key')) {
608 | 					console.log(
609 | 						chalk.yellow(
610 | 							'\nPlease ensure your API keys are correctly configured in .env or ~/.taskmaster/.env'
611 | 						)
612 | 					);
613 | 					console.log(
614 | 						chalk.yellow("Run 'task-master models --setup' if needed.")
615 | 					);
616 | 				}
617 | 			}
618 | 			throw aiError;
619 | 		}
620 | 	} catch (error) {
621 | 		reportLog(`Error analyzing task complexity: ${error.message}`, 'error');
622 | 		if (outputFormat === 'text') {
623 | 			console.error(
624 | 				chalk.red(`Error analyzing task complexity: ${error.message}`)
625 | 			);
626 | 			if (getDebugFlag(session)) {
627 | 				console.error(error);
628 | 			}
629 | 			process.exit(1);
630 | 		} else {
631 | 			throw error;
632 | 		}
633 | 	}
634 | }
635 | 
636 | export default analyzeTaskComplexity;
637 | 
```

--------------------------------------------------------------------------------
/tests/unit/ai-providers/gemini-cli.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | 
  3 | // Mock the ai module
  4 | jest.unstable_mockModule('ai', () => ({
  5 | 	generateObject: jest.fn(),
  6 | 	generateText: jest.fn(),
  7 | 	streamText: jest.fn()
  8 | }));
  9 | 
 10 | // Mock the gemini-cli SDK module
 11 | jest.unstable_mockModule('ai-sdk-provider-gemini-cli', () => ({
 12 | 	createGeminiProvider: jest.fn((options) => {
 13 | 		const provider = (modelId, settings) => ({
 14 | 			// Mock language model
 15 | 			id: modelId,
 16 | 			settings,
 17 | 			authOptions: options
 18 | 		});
 19 | 		provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
 20 | 		provider.chat = provider.languageModel;
 21 | 		return provider;
 22 | 	})
 23 | }));
 24 | 
 25 | // Mock the base provider
 26 | jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
 27 | 	BaseAIProvider: class {
 28 | 		constructor() {
 29 | 			this.name = 'Base Provider';
 30 | 		}
 31 | 		handleError(context, error) {
 32 | 			throw error;
 33 | 		}
 34 | 		validateParams(params) {
 35 | 			// Basic validation
 36 | 			if (!params.modelId) {
 37 | 				throw new Error('Model ID is required');
 38 | 			}
 39 | 		}
 40 | 		validateMessages(messages) {
 41 | 			if (!messages || !Array.isArray(messages)) {
 42 | 				throw new Error('Invalid messages array');
 43 | 			}
 44 | 		}
 45 | 		async generateObject(params) {
 46 | 			// Mock implementation that can be overridden
 47 | 			throw new Error('Mock base generateObject error');
 48 | 		}
 49 | 	}
 50 | }));
 51 | 
 52 | // Mock the log module
 53 | jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
 54 | 	log: jest.fn()
 55 | }));
 56 | 
 57 | // Import after mocking
 58 | const { GeminiCliProvider } = await import(
 59 | 	'../../../src/ai-providers/gemini-cli.js'
 60 | );
 61 | const { createGeminiProvider } = await import('ai-sdk-provider-gemini-cli');
 62 | const { generateObject, generateText, streamText } = await import('ai');
 63 | const { log } = await import('../../../scripts/modules/utils.js');
 64 | 
 65 | describe('GeminiCliProvider', () => {
 66 | 	let provider;
 67 | 	let consoleLogSpy;
 68 | 
 69 | 	beforeEach(() => {
 70 | 		provider = new GeminiCliProvider();
 71 | 		jest.clearAllMocks();
 72 | 		consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
 73 | 	});
 74 | 
 75 | 	afterEach(() => {
 76 | 		consoleLogSpy.mockRestore();
 77 | 	});
 78 | 
 79 | 	describe('constructor', () => {
 80 | 		it('should set the provider name to Gemini CLI', () => {
 81 | 			expect(provider.name).toBe('Gemini CLI');
 82 | 		});
 83 | 	});
 84 | 
 85 | 	describe('validateAuth', () => {
 86 | 		it('should not throw an error when API key is provided', () => {
 87 | 			expect(() => provider.validateAuth({ apiKey: 'test-key' })).not.toThrow();
 88 | 			expect(consoleLogSpy).not.toHaveBeenCalled();
 89 | 		});
 90 | 
 91 | 		it('should not require API key and should not log messages', () => {
 92 | 			expect(() => provider.validateAuth({})).not.toThrow();
 93 | 			expect(consoleLogSpy).not.toHaveBeenCalled();
 94 | 		});
 95 | 
 96 | 		it('should not require any parameters', () => {
 97 | 			expect(() => provider.validateAuth()).not.toThrow();
 98 | 			expect(consoleLogSpy).not.toHaveBeenCalled();
 99 | 		});
100 | 	});
101 | 
102 | 	describe('getClient', () => {
103 | 		it('should return a gemini client with API key auth when apiKey is provided', async () => {
104 | 			const client = await provider.getClient({ apiKey: 'test-api-key' });
105 | 
106 | 			expect(client).toBeDefined();
107 | 			expect(typeof client).toBe('function');
108 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
109 | 				authType: 'api-key',
110 | 				apiKey: 'test-api-key'
111 | 			});
112 | 		});
113 | 
114 | 		it('should return a gemini client with OAuth auth when no apiKey is provided', async () => {
115 | 			const client = await provider.getClient({});
116 | 
117 | 			expect(client).toBeDefined();
118 | 			expect(typeof client).toBe('function');
119 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
120 | 				authType: 'oauth-personal'
121 | 			});
122 | 		});
123 | 
124 | 		it('should include baseURL when provided', async () => {
125 | 			const client = await provider.getClient({
126 | 				apiKey: 'test-key',
127 | 				baseURL: 'https://custom-endpoint.com'
128 | 			});
129 | 
130 | 			expect(client).toBeDefined();
131 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
132 | 				authType: 'api-key',
133 | 				apiKey: 'test-key',
134 | 				baseURL: 'https://custom-endpoint.com'
135 | 			});
136 | 		});
137 | 
138 | 		it('should have languageModel and chat methods', async () => {
139 | 			const client = await provider.getClient({ apiKey: 'test-key' });
140 | 			expect(client.languageModel).toBeDefined();
141 | 			expect(client.chat).toBeDefined();
142 | 			expect(client.chat).toBe(client.languageModel);
143 | 		});
144 | 	});
145 | 
146 | 	describe('_extractSystemMessage', () => {
147 | 		it('should extract single system message', () => {
148 | 			const messages = [
149 | 				{ role: 'system', content: 'You are a helpful assistant' },
150 | 				{ role: 'user', content: 'Hello' }
151 | 			];
152 | 			const result = provider._extractSystemMessage(messages);
153 | 			expect(result.systemPrompt).toBe('You are a helpful assistant');
154 | 			expect(result.messages).toEqual([{ role: 'user', content: 'Hello' }]);
155 | 		});
156 | 
157 | 		it('should combine multiple system messages', () => {
158 | 			const messages = [
159 | 				{ role: 'system', content: 'You are helpful' },
160 | 				{ role: 'system', content: 'Be concise' },
161 | 				{ role: 'user', content: 'Hello' }
162 | 			];
163 | 			const result = provider._extractSystemMessage(messages);
164 | 			expect(result.systemPrompt).toBe('You are helpful\n\nBe concise');
165 | 			expect(result.messages).toEqual([{ role: 'user', content: 'Hello' }]);
166 | 		});
167 | 
168 | 		it('should handle messages without system prompts', () => {
169 | 			const messages = [
170 | 				{ role: 'user', content: 'Hello' },
171 | 				{ role: 'assistant', content: 'Hi there' }
172 | 			];
173 | 			const result = provider._extractSystemMessage(messages);
174 | 			expect(result.systemPrompt).toBeUndefined();
175 | 			expect(result.messages).toEqual(messages);
176 | 		});
177 | 
178 | 		it('should handle empty or invalid input', () => {
179 | 			expect(provider._extractSystemMessage([])).toEqual({
180 | 				systemPrompt: undefined,
181 | 				messages: []
182 | 			});
183 | 			expect(provider._extractSystemMessage(null)).toEqual({
184 | 				systemPrompt: undefined,
185 | 				messages: []
186 | 			});
187 | 			expect(provider._extractSystemMessage(undefined)).toEqual({
188 | 				systemPrompt: undefined,
189 | 				messages: []
190 | 			});
191 | 		});
192 | 
193 | 		it('should add JSON enforcement when enforceJsonOutput is true', () => {
194 | 			const messages = [
195 | 				{ role: 'system', content: 'You are a helpful assistant' },
196 | 				{ role: 'user', content: 'Hello' }
197 | 			];
198 | 			const result = provider._extractSystemMessage(messages, {
199 | 				enforceJsonOutput: true
200 | 			});
201 | 			expect(result.systemPrompt).toContain('You are a helpful assistant');
202 | 			expect(result.systemPrompt).toContain(
203 | 				'CRITICAL: You MUST respond with ONLY valid JSON'
204 | 			);
205 | 			expect(result.messages).toEqual([{ role: 'user', content: 'Hello' }]);
206 | 		});
207 | 
208 | 		it('should add JSON enforcement with no existing system message', () => {
209 | 			const messages = [{ role: 'user', content: 'Return JSON format' }];
210 | 			const result = provider._extractSystemMessage(messages, {
211 | 				enforceJsonOutput: true
212 | 			});
213 | 			expect(result.systemPrompt).toBe(
214 | 				'CRITICAL: You MUST respond with ONLY valid JSON. Do not include any explanatory text, markdown formatting, code block markers, or conversational phrases like "Here is" or "Of course". Your entire response must be parseable JSON that starts with { or [ and ends with } or ]. No exceptions.'
215 | 			);
216 | 			expect(result.messages).toEqual([
217 | 				{ role: 'user', content: 'Return JSON format' }
218 | 			]);
219 | 		});
220 | 	});
221 | 
222 | 	describe('_detectJsonRequest', () => {
223 | 		it('should detect JSON requests from user messages', () => {
224 | 			const messages = [
225 | 				{
226 | 					role: 'user',
227 | 					content: 'Please return JSON format with subtasks array'
228 | 				}
229 | 			];
230 | 			expect(provider._detectJsonRequest(messages)).toBe(true);
231 | 		});
232 | 
233 | 		it('should detect various JSON indicators', () => {
234 | 			const testCases = [
235 | 				'respond only with valid JSON',
236 | 				'return JSON format',
237 | 				'output schema: {"test": true}',
238 | 				'format: [{"id": 1}]',
239 | 				'Please return subtasks in array format',
240 | 				'Return an object with properties'
241 | 			];
242 | 
243 | 			testCases.forEach((content) => {
244 | 				const messages = [{ role: 'user', content }];
245 | 				expect(provider._detectJsonRequest(messages)).toBe(true);
246 | 			});
247 | 		});
248 | 
249 | 		it('should not detect JSON requests for regular conversation', () => {
250 | 			const messages = [{ role: 'user', content: 'Hello, how are you today?' }];
251 | 			expect(provider._detectJsonRequest(messages)).toBe(false);
252 | 		});
253 | 
254 | 		it('should handle multiple user messages', () => {
255 | 			const messages = [
256 | 				{ role: 'user', content: 'Hello' },
257 | 				{ role: 'assistant', content: 'Hi there' },
258 | 				{ role: 'user', content: 'Now please return JSON format' }
259 | 			];
260 | 			expect(provider._detectJsonRequest(messages)).toBe(true);
261 | 		});
262 | 	});
263 | 
264 | 	describe('_getJsonEnforcementPrompt', () => {
265 | 		it('should return strict JSON enforcement prompt', () => {
266 | 			const prompt = provider._getJsonEnforcementPrompt();
267 | 			expect(prompt).toContain('CRITICAL');
268 | 			expect(prompt).toContain('ONLY valid JSON');
269 | 			expect(prompt).toContain('No exceptions');
270 | 		});
271 | 	});
272 | 
273 | 	describe('_isValidJson', () => {
274 | 		it('should return true for valid JSON objects', () => {
275 | 			expect(provider._isValidJson('{"test": true}')).toBe(true);
276 | 			expect(provider._isValidJson('{"subtasks": [{"id": 1}]}')).toBe(true);
277 | 		});
278 | 
279 | 		it('should return true for valid JSON arrays', () => {
280 | 			expect(provider._isValidJson('[1, 2, 3]')).toBe(true);
281 | 			expect(provider._isValidJson('[{"id": 1}, {"id": 2}]')).toBe(true);
282 | 		});
283 | 
284 | 		it('should return false for invalid JSON', () => {
285 | 			expect(provider._isValidJson('Of course. Here is...')).toBe(false);
286 | 			expect(provider._isValidJson('{"invalid": json}')).toBe(false);
287 | 			expect(provider._isValidJson('not json at all')).toBe(false);
288 | 		});
289 | 
290 | 		it('should handle edge cases', () => {
291 | 			expect(provider._isValidJson('')).toBe(false);
292 | 			expect(provider._isValidJson(null)).toBe(false);
293 | 			expect(provider._isValidJson(undefined)).toBe(false);
294 | 			expect(provider._isValidJson('   {"test": true}   ')).toBe(true); // with whitespace
295 | 		});
296 | 	});
297 | 
298 | 	describe('extractJson', () => {
299 | 		it('should extract JSON from markdown code blocks', () => {
300 | 			const input = '```json\n{"subtasks": [{"id": 1}]}\n```';
301 | 			const result = provider.extractJson(input);
302 | 			const parsed = JSON.parse(result);
303 | 			expect(parsed).toEqual({ subtasks: [{ id: 1 }] });
304 | 		});
305 | 
306 | 		it('should extract JSON with explanatory text', () => {
307 | 			const input = 'Here\'s the JSON response:\n{"subtasks": [{"id": 1}]}';
308 | 			const result = provider.extractJson(input);
309 | 			const parsed = JSON.parse(result);
310 | 			expect(parsed).toEqual({ subtasks: [{ id: 1 }] });
311 | 		});
312 | 
313 | 		it('should handle variable declarations', () => {
314 | 			const input = 'const result = {"subtasks": [{"id": 1}]};';
315 | 			const result = provider.extractJson(input);
316 | 			const parsed = JSON.parse(result);
317 | 			expect(parsed).toEqual({ subtasks: [{ id: 1 }] });
318 | 		});
319 | 
320 | 		it('should handle trailing commas with jsonc-parser', () => {
321 | 			const input = '{"subtasks": [{"id": 1,}],}';
322 | 			const result = provider.extractJson(input);
323 | 			const parsed = JSON.parse(result);
324 | 			expect(parsed).toEqual({ subtasks: [{ id: 1 }] });
325 | 		});
326 | 
327 | 		it('should handle arrays', () => {
328 | 			const input = 'The result is: [1, 2, 3]';
329 | 			const result = provider.extractJson(input);
330 | 			const parsed = JSON.parse(result);
331 | 			expect(parsed).toEqual([1, 2, 3]);
332 | 		});
333 | 
334 | 		it('should handle nested objects with proper bracket matching', () => {
335 | 			const input =
336 | 				'Response: {"outer": {"inner": {"value": "test"}}} extra text';
337 | 			const result = provider.extractJson(input);
338 | 			const parsed = JSON.parse(result);
339 | 			expect(parsed).toEqual({ outer: { inner: { value: 'test' } } });
340 | 		});
341 | 
342 | 		it('should handle escaped quotes in strings', () => {
343 | 			const input = '{"message": "He said \\"hello\\" to me"}';
344 | 			const result = provider.extractJson(input);
345 | 			const parsed = JSON.parse(result);
346 | 			expect(parsed).toEqual({ message: 'He said "hello" to me' });
347 | 		});
348 | 
349 | 		it('should return original text if no JSON found', () => {
350 | 			const input = 'No JSON here';
351 | 			expect(provider.extractJson(input)).toBe(input);
352 | 		});
353 | 
354 | 		it('should handle null or non-string input', () => {
355 | 			expect(provider.extractJson(null)).toBe(null);
356 | 			expect(provider.extractJson(undefined)).toBe(undefined);
357 | 			expect(provider.extractJson(123)).toBe(123);
358 | 		});
359 | 
360 | 		it('should handle partial JSON by finding valid boundaries', () => {
361 | 			const input = '{"valid": true, "partial": "incomplete';
362 | 			// Should return original text since no valid JSON can be extracted
363 | 			expect(provider.extractJson(input)).toBe(input);
364 | 		});
365 | 
366 | 		it('should handle performance edge cases with large text', () => {
367 | 			// Test with large text that has JSON at the end
368 | 			const largePrefix = 'This is a very long explanation. '.repeat(1000);
369 | 			const json = '{"result": "success"}';
370 | 			const input = largePrefix + json;
371 | 
372 | 			const result = provider.extractJson(input);
373 | 			const parsed = JSON.parse(result);
374 | 			expect(parsed).toEqual({ result: 'success' });
375 | 		});
376 | 
377 | 		it('should handle early termination for very large invalid content', () => {
378 | 			// Test that it doesn't hang on very large content without JSON
379 | 			const largeText = 'No JSON here. '.repeat(2000);
380 | 			const result = provider.extractJson(largeText);
381 | 			expect(result).toBe(largeText);
382 | 		});
383 | 	});
384 | 
385 | 	describe('generateObject', () => {
386 | 		const mockParams = {
387 | 			modelId: 'gemini-2.0-flash-exp',
388 | 			apiKey: 'test-key',
389 | 			messages: [{ role: 'user', content: 'Test message' }],
390 | 			schema: { type: 'object', properties: {} },
391 | 			objectName: 'testObject'
392 | 		};
393 | 
394 | 		beforeEach(() => {
395 | 			jest.clearAllMocks();
396 | 		});
397 | 
398 | 		it('should handle JSON parsing errors by attempting manual extraction', async () => {
399 | 			// Mock the parent generateObject to throw a JSON parsing error
400 | 			jest
401 | 				.spyOn(
402 | 					Object.getPrototypeOf(Object.getPrototypeOf(provider)),
403 | 					'generateObject'
404 | 				)
405 | 				.mockRejectedValueOnce(new Error('Failed to parse JSON response'));
406 | 
407 | 			// Mock generateObject from ai module to return text with JSON
408 | 			generateObject.mockResolvedValueOnce({
409 | 				rawResponse: {
410 | 					text: 'Here is the JSON:\n```json\n{"subtasks": [{"id": 1}]}\n```'
411 | 				},
412 | 				object: null,
413 | 				usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 }
414 | 			});
415 | 
416 | 			const result = await provider.generateObject(mockParams);
417 | 
418 | 			expect(log).toHaveBeenCalledWith(
419 | 				'debug',
420 | 				expect.stringContaining('attempting manual extraction')
421 | 			);
422 | 			expect(generateObject).toHaveBeenCalledWith({
423 | 				model: expect.objectContaining({
424 | 					id: 'gemini-2.0-flash-exp',
425 | 					authOptions: expect.objectContaining({
426 | 						authType: 'api-key',
427 | 						apiKey: 'test-key'
428 | 					})
429 | 				}),
430 | 				messages: mockParams.messages,
431 | 				schema: mockParams.schema,
432 | 				mode: 'json', // Should use json mode for Gemini
433 | 				system: expect.stringContaining(
434 | 					'CRITICAL: You MUST respond with ONLY valid JSON'
435 | 				),
436 | 				maxTokens: undefined,
437 | 				temperature: undefined
438 | 			});
439 | 			expect(result.object).toEqual({ subtasks: [{ id: 1 }] });
440 | 		});
441 | 
442 | 		it('should throw error if manual extraction also fails', async () => {
443 | 			// Mock parent to throw JSON error
444 | 			jest
445 | 				.spyOn(
446 | 					Object.getPrototypeOf(Object.getPrototypeOf(provider)),
447 | 					'generateObject'
448 | 				)
449 | 				.mockRejectedValueOnce(new Error('Failed to parse JSON'));
450 | 
451 | 			// Mock generateObject to return unparseable text
452 | 			generateObject.mockResolvedValueOnce({
453 | 				rawResponse: { text: 'Not valid JSON at all' },
454 | 				object: null
455 | 			});
456 | 
457 | 			await expect(provider.generateObject(mockParams)).rejects.toThrow(
458 | 				'Gemini CLI failed to generate valid JSON object: Failed to parse JSON'
459 | 			);
460 | 		});
461 | 
462 | 		it('should pass through non-JSON errors unchanged', async () => {
463 | 			const otherError = new Error('Network error');
464 | 			jest
465 | 				.spyOn(
466 | 					Object.getPrototypeOf(Object.getPrototypeOf(provider)),
467 | 					'generateObject'
468 | 				)
469 | 				.mockRejectedValueOnce(otherError);
470 | 
471 | 			await expect(provider.generateObject(mockParams)).rejects.toThrow(
472 | 				'Network error'
473 | 			);
474 | 			expect(generateObject).not.toHaveBeenCalled();
475 | 		});
476 | 
477 | 		it('should handle successful response from parent', async () => {
478 | 			const mockResult = {
479 | 				object: { test: 'data' },
480 | 				usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 }
481 | 			};
482 | 			jest
483 | 				.spyOn(
484 | 					Object.getPrototypeOf(Object.getPrototypeOf(provider)),
485 | 					'generateObject'
486 | 				)
487 | 				.mockResolvedValueOnce(mockResult);
488 | 
489 | 			const result = await provider.generateObject(mockParams);
490 | 			expect(result).toEqual(mockResult);
491 | 			expect(generateObject).not.toHaveBeenCalled();
492 | 		});
493 | 	});
494 | 
495 | 	describe('system message support', () => {
496 | 		const mockParams = {
497 | 			modelId: 'gemini-2.0-flash-exp',
498 | 			apiKey: 'test-key',
499 | 			messages: [
500 | 				{ role: 'system', content: 'You are a helpful assistant' },
501 | 				{ role: 'user', content: 'Hello' }
502 | 			],
503 | 			maxTokens: 100,
504 | 			temperature: 0.7
505 | 		};
506 | 
507 | 		describe('generateText with system messages', () => {
508 | 			beforeEach(() => {
509 | 				jest.clearAllMocks();
510 | 			});
511 | 
512 | 			it('should pass system prompt separately to AI SDK', async () => {
513 | 				const { generateText } = await import('ai');
514 | 				generateText.mockResolvedValueOnce({
515 | 					text: 'Hello! How can I help you?',
516 | 					usage: { promptTokens: 10, completionTokens: 8, totalTokens: 18 }
517 | 				});
518 | 
519 | 				const result = await provider.generateText(mockParams);
520 | 
521 | 				expect(generateText).toHaveBeenCalledWith({
522 | 					model: expect.objectContaining({
523 | 						id: 'gemini-2.0-flash-exp'
524 | 					}),
525 | 					system: 'You are a helpful assistant',
526 | 					messages: [{ role: 'user', content: 'Hello' }],
527 | 					maxOutputTokens: 100,
528 | 					temperature: 0.7
529 | 				});
530 | 				expect(result.text).toBe('Hello! How can I help you?');
531 | 			});
532 | 
533 | 			it('should handle messages without system prompt', async () => {
534 | 				const { generateText } = await import('ai');
535 | 				const paramsNoSystem = {
536 | 					...mockParams,
537 | 					messages: [{ role: 'user', content: 'Hello' }]
538 | 				};
539 | 
540 | 				generateText.mockResolvedValueOnce({
541 | 					text: 'Hi there!',
542 | 					usage: { promptTokens: 5, completionTokens: 3, totalTokens: 8 }
543 | 				});
544 | 
545 | 				await provider.generateText(paramsNoSystem);
546 | 
547 | 				expect(generateText).toHaveBeenCalledWith({
548 | 					model: expect.objectContaining({
549 | 						id: 'gemini-2.0-flash-exp'
550 | 					}),
551 | 					system: undefined,
552 | 					messages: [{ role: 'user', content: 'Hello' }],
553 | 					maxOutputTokens: 100,
554 | 					temperature: 0.7
555 | 				});
556 | 			});
557 | 		});
558 | 
559 | 		describe('streamText with system messages', () => {
560 | 			it('should pass system prompt separately to AI SDK', async () => {
561 | 				const { streamText } = await import('ai');
562 | 				const mockStream = { stream: 'mock-stream' };
563 | 				streamText.mockResolvedValueOnce(mockStream);
564 | 
565 | 				const result = await provider.streamText(mockParams);
566 | 
567 | 				expect(streamText).toHaveBeenCalledWith({
568 | 					model: expect.objectContaining({
569 | 						id: 'gemini-2.0-flash-exp'
570 | 					}),
571 | 					system: 'You are a helpful assistant',
572 | 					messages: [{ role: 'user', content: 'Hello' }],
573 | 					maxOutputTokens: 100,
574 | 					temperature: 0.7
575 | 				});
576 | 				expect(result).toBe(mockStream);
577 | 			});
578 | 		});
579 | 
580 | 		describe('generateObject with system messages', () => {
581 | 			const mockObjectParams = {
582 | 				...mockParams,
583 | 				schema: { type: 'object', properties: {} },
584 | 				objectName: 'testObject'
585 | 			};
586 | 
587 | 			it('should include system prompt in fallback generateObject call', async () => {
588 | 				// Mock parent to throw JSON error
589 | 				jest
590 | 					.spyOn(
591 | 						Object.getPrototypeOf(Object.getPrototypeOf(provider)),
592 | 						'generateObject'
593 | 					)
594 | 					.mockRejectedValueOnce(new Error('Failed to parse JSON'));
595 | 
596 | 				// Mock direct generateObject call
597 | 				generateObject.mockResolvedValueOnce({
598 | 					object: { result: 'success' },
599 | 					usage: { promptTokens: 15, completionTokens: 10, totalTokens: 25 }
600 | 				});
601 | 
602 | 				const result = await provider.generateObject(mockObjectParams);
603 | 
604 | 				expect(generateObject).toHaveBeenCalledWith({
605 | 					model: expect.objectContaining({
606 | 						id: 'gemini-2.0-flash-exp'
607 | 					}),
608 | 					system: expect.stringContaining('You are a helpful assistant'),
609 | 					messages: [{ role: 'user', content: 'Hello' }],
610 | 					schema: mockObjectParams.schema,
611 | 					mode: 'json',
612 | 					maxOutputTokens: 100,
613 | 					temperature: 0.7
614 | 				});
615 | 				expect(result.object).toEqual({ result: 'success' });
616 | 			});
617 | 		});
618 | 	});
619 | 
620 | 	// Note: Error handling for module loading is tested in integration tests
621 | 	// since dynamic imports are difficult to mock properly in unit tests
622 | 
623 | 	describe('authentication scenarios', () => {
624 | 		it('should use api-key auth type with API key', async () => {
625 | 			await provider.getClient({ apiKey: 'gemini-test-key' });
626 | 
627 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
628 | 				authType: 'api-key',
629 | 				apiKey: 'gemini-test-key'
630 | 			});
631 | 		});
632 | 
633 | 		it('should use oauth-personal auth type without API key', async () => {
634 | 			await provider.getClient({});
635 | 
636 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
637 | 				authType: 'oauth-personal'
638 | 			});
639 | 		});
640 | 
641 | 		it('should handle empty string API key as no API key', async () => {
642 | 			await provider.getClient({ apiKey: '' });
643 | 
644 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
645 | 				authType: 'oauth-personal'
646 | 			});
647 | 		});
648 | 	});
649 | });
650 | 
```

--------------------------------------------------------------------------------
/apps/extension/src/utils/errorHandler.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import * as vscode from 'vscode';
  2 | import { logger } from './logger';
  3 | import {
  4 | 	getNotificationType,
  5 | 	getToastDuration,
  6 | 	shouldShowNotification
  7 | } from './notificationPreferences';
  8 | 
  9 | export enum ErrorSeverity {
 10 | 	LOW = 'low',
 11 | 	MEDIUM = 'medium',
 12 | 	HIGH = 'high',
 13 | 	CRITICAL = 'critical'
 14 | }
 15 | 
 16 | export enum ErrorCategory {
 17 | 	MCP_CONNECTION = 'mcp_connection',
 18 | 	CONFIGURATION = 'configuration',
 19 | 	TASK_LOADING = 'task_loading',
 20 | 	UI_RENDERING = 'ui_rendering',
 21 | 	VALIDATION = 'validation',
 22 | 	NETWORK = 'network',
 23 | 	INTERNAL = 'internal',
 24 | 	TASK_MASTER_API = 'TASK_MASTER_API',
 25 | 	DATA_VALIDATION = 'DATA_VALIDATION',
 26 | 	DATA_PARSING = 'DATA_PARSING',
 27 | 	TASK_DATA_CORRUPTION = 'TASK_DATA_CORRUPTION',
 28 | 	VSCODE_API = 'VSCODE_API',
 29 | 	WEBVIEW = 'WEBVIEW',
 30 | 	EXTENSION_HOST = 'EXTENSION_HOST',
 31 | 	USER_INTERACTION = 'USER_INTERACTION',
 32 | 	DRAG_DROP = 'DRAG_DROP',
 33 | 	COMPONENT_RENDER = 'COMPONENT_RENDER',
 34 | 	PERMISSION = 'PERMISSION',
 35 | 	FILE_SYSTEM = 'FILE_SYSTEM',
 36 | 	UNKNOWN = 'UNKNOWN'
 37 | }
 38 | 
 39 | export enum NotificationType {
 40 | 	VSCODE_INFO = 'VSCODE_INFO',
 41 | 	VSCODE_WARNING = 'VSCODE_WARNING',
 42 | 	VSCODE_ERROR = 'VSCODE_ERROR',
 43 | 	TOAST_SUCCESS = 'TOAST_SUCCESS',
 44 | 	TOAST_INFO = 'TOAST_INFO',
 45 | 	TOAST_WARNING = 'TOAST_WARNING',
 46 | 	TOAST_ERROR = 'TOAST_ERROR',
 47 | 	CONSOLE_ONLY = 'CONSOLE_ONLY',
 48 | 	SILENT = 'SILENT'
 49 | }
 50 | 
 51 | export interface ErrorContext {
 52 | 	// Core error information
 53 | 	category: ErrorCategory;
 54 | 	severity: ErrorSeverity;
 55 | 	message: string;
 56 | 	originalError?: Error | unknown;
 57 | 
 58 | 	// Contextual information
 59 | 	operation?: string; // What operation was being performed
 60 | 	taskId?: string; // Related task ID if applicable
 61 | 	userId?: string; // User context if applicable
 62 | 	sessionId?: string; // Session context
 63 | 
 64 | 	// Technical details
 65 | 	stackTrace?: string;
 66 | 	userAgent?: string;
 67 | 	timestamp?: number;
 68 | 
 69 | 	// Recovery information
 70 | 	isRecoverable?: boolean;
 71 | 	suggestedActions?: string[];
 72 | 	documentationLink?: string;
 73 | 
 74 | 	// Notification preferences
 75 | 	notificationType?: NotificationType;
 76 | 	showToUser?: boolean;
 77 | 	logToConsole?: boolean;
 78 | 	logToFile?: boolean;
 79 | }
 80 | 
 81 | export interface ErrorDetails {
 82 | 	code: string;
 83 | 	message: string;
 84 | 	category: ErrorCategory;
 85 | 	severity: ErrorSeverity;
 86 | 	timestamp: Date;
 87 | 	context?: Record<string, any>;
 88 | 	stack?: string;
 89 | 	userAction?: string;
 90 | 	recovery?: {
 91 | 		automatic: boolean;
 92 | 		action?: () => Promise<void>;
 93 | 		description?: string;
 94 | 	};
 95 | }
 96 | 
 97 | export interface ErrorLogEntry {
 98 | 	id: string;
 99 | 	error: ErrorDetails;
100 | 	resolved: boolean;
101 | 	resolvedAt?: Date;
102 | 	attempts: number;
103 | 	lastAttempt?: Date;
104 | }
105 | 
106 | /**
107 |  * Base class for all Task Master errors
108 |  */
109 | export abstract class TaskMasterError extends Error {
110 | 	public readonly code: string;
111 | 	public readonly category: ErrorCategory;
112 | 	public readonly severity: ErrorSeverity;
113 | 	public readonly timestamp: Date;
114 | 	public readonly context?: Record<string, any>;
115 | 	public readonly userAction?: string;
116 | 	public readonly recovery?: {
117 | 		automatic: boolean;
118 | 		action?: () => Promise<void>;
119 | 		description?: string;
120 | 	};
121 | 
122 | 	constructor(
123 | 		message: string,
124 | 		code: string,
125 | 		category: ErrorCategory,
126 | 		severity: ErrorSeverity = ErrorSeverity.MEDIUM,
127 | 		context?: Record<string, any>,
128 | 		userAction?: string,
129 | 		recovery?: {
130 | 			automatic: boolean;
131 | 			action?: () => Promise<void>;
132 | 			description?: string;
133 | 		}
134 | 	) {
135 | 		super(message);
136 | 		this.name = this.constructor.name;
137 | 		this.code = code;
138 | 		this.category = category;
139 | 		this.severity = severity;
140 | 		this.timestamp = new Date();
141 | 		this.context = context;
142 | 		this.userAction = userAction;
143 | 		this.recovery = recovery;
144 | 
145 | 		// Capture stack trace
146 | 		if (Error.captureStackTrace) {
147 | 			Error.captureStackTrace(this, this.constructor);
148 | 		}
149 | 	}
150 | 
151 | 	public toErrorDetails(): ErrorDetails {
152 | 		return {
153 | 			code: this.code,
154 | 			message: this.message,
155 | 			category: this.category,
156 | 			severity: this.severity,
157 | 			timestamp: this.timestamp,
158 | 			context: this.context,
159 | 			stack: this.stack,
160 | 			userAction: this.userAction,
161 | 			recovery: this.recovery
162 | 		};
163 | 	}
164 | }
165 | 
166 | /**
167 |  * MCP Connection related errors
168 |  */
169 | export class MCPConnectionError extends TaskMasterError {
170 | 	constructor(
171 | 		message: string,
172 | 		code = 'MCP_CONNECTION_FAILED',
173 | 		context?: Record<string, any>,
174 | 		recovery?: {
175 | 			automatic: boolean;
176 | 			action?: () => Promise<void>;
177 | 			description?: string;
178 | 		}
179 | 	) {
180 | 		super(
181 | 			message,
182 | 			code,
183 | 			ErrorCategory.MCP_CONNECTION,
184 | 			ErrorSeverity.HIGH,
185 | 			context,
186 | 			'Check your Task Master configuration and ensure the MCP server is accessible.',
187 | 			recovery
188 | 		);
189 | 	}
190 | }
191 | 
192 | /**
193 |  * Configuration related errors
194 |  */
195 | export class ConfigurationError extends TaskMasterError {
196 | 	constructor(
197 | 		message: string,
198 | 		code = 'CONFIGURATION_INVALID',
199 | 		context?: Record<string, any>
200 | 	) {
201 | 		super(
202 | 			message,
203 | 			code,
204 | 			ErrorCategory.CONFIGURATION,
205 | 			ErrorSeverity.MEDIUM,
206 | 			context,
207 | 			'Check your Task Master configuration in VS Code settings.'
208 | 		);
209 | 	}
210 | }
211 | 
212 | /**
213 |  * Task loading related errors
214 |  */
215 | export class TaskLoadingError extends TaskMasterError {
216 | 	constructor(
217 | 		message: string,
218 | 		code = 'TASK_LOADING_FAILED',
219 | 		context?: Record<string, any>,
220 | 		recovery?: {
221 | 			automatic: boolean;
222 | 			action?: () => Promise<void>;
223 | 			description?: string;
224 | 		}
225 | 	) {
226 | 		super(
227 | 			message,
228 | 			code,
229 | 			ErrorCategory.TASK_LOADING,
230 | 			ErrorSeverity.MEDIUM,
231 | 			context,
232 | 			'Try refreshing the task list or check your project configuration.',
233 | 			recovery
234 | 		);
235 | 	}
236 | }
237 | 
238 | /**
239 |  * UI rendering related errors
240 |  */
241 | export class UIRenderingError extends TaskMasterError {
242 | 	constructor(
243 | 		message: string,
244 | 		code = 'UI_RENDERING_FAILED',
245 | 		context?: Record<string, any>
246 | 	) {
247 | 		super(
248 | 			message,
249 | 			code,
250 | 			ErrorCategory.UI_RENDERING,
251 | 			ErrorSeverity.LOW,
252 | 			context,
253 | 			'Try closing and reopening the Kanban board.'
254 | 		);
255 | 	}
256 | }
257 | 
258 | /**
259 |  * Network related errors
260 |  */
261 | export class NetworkError extends TaskMasterError {
262 | 	constructor(
263 | 		message: string,
264 | 		code = 'NETWORK_ERROR',
265 | 		context?: Record<string, any>,
266 | 		recovery?: {
267 | 			automatic: boolean;
268 | 			action?: () => Promise<void>;
269 | 			description?: string;
270 | 		}
271 | 	) {
272 | 		super(
273 | 			message,
274 | 			code,
275 | 			ErrorCategory.NETWORK,
276 | 			ErrorSeverity.MEDIUM,
277 | 			context,
278 | 			'Check your network connection and firewall settings.',
279 | 			recovery
280 | 		);
281 | 	}
282 | }
283 | 
284 | /**
285 |  * Centralized error handler
286 |  */
287 | export class ErrorHandler {
288 | 	private static instance: ErrorHandler | null = null;
289 | 	private errorLog: ErrorLogEntry[] = [];
290 | 	private maxLogSize = 1000;
291 | 	private errorListeners: ((error: ErrorDetails) => void)[] = [];
292 | 
293 | 	private constructor() {
294 | 		this.setupGlobalErrorHandlers();
295 | 	}
296 | 
297 | 	static getInstance(): ErrorHandler {
298 | 		if (!ErrorHandler.instance) {
299 | 			ErrorHandler.instance = new ErrorHandler();
300 | 		}
301 | 		return ErrorHandler.instance;
302 | 	}
303 | 
304 | 	/**
305 | 	 * Handle an error with comprehensive logging and recovery
306 | 	 */
307 | 	async handleError(
308 | 		error: Error | TaskMasterError,
309 | 		context?: Record<string, any>
310 | 	): Promise<void> {
311 | 		const errorDetails = this.createErrorDetails(error, context);
312 | 		const logEntry = this.logError(errorDetails);
313 | 
314 | 		// Notify listeners
315 | 		this.notifyErrorListeners(errorDetails);
316 | 
317 | 		// Show user notification based on severity
318 | 		await this.showUserNotification(errorDetails);
319 | 
320 | 		// Attempt recovery if available
321 | 		if (errorDetails.recovery?.automatic && errorDetails.recovery.action) {
322 | 			try {
323 | 				await errorDetails.recovery.action();
324 | 				this.markErrorResolved(logEntry.id);
325 | 			} catch (recoveryError) {
326 | 				logger.error('Error recovery failed:', recoveryError);
327 | 				logEntry.attempts++;
328 | 				logEntry.lastAttempt = new Date();
329 | 			}
330 | 		}
331 | 
332 | 		// Log to console with appropriate level
333 | 		this.logToConsole(errorDetails);
334 | 	}
335 | 
336 | 	/**
337 | 	 * Handle critical errors that should stop execution
338 | 	 */
339 | 	async handleCriticalError(
340 | 		error: Error | TaskMasterError,
341 | 		context?: Record<string, any>
342 | 	): Promise<void> {
343 | 		const errorDetails = this.createErrorDetails(error, context);
344 | 		errorDetails.severity = ErrorSeverity.CRITICAL;
345 | 
346 | 		await this.handleError(error, context);
347 | 
348 | 		// Show critical error dialog
349 | 		const action = await vscode.window.showErrorMessage(
350 | 			`Critical Error in Task Master: ${errorDetails.message}`,
351 | 			'View Details',
352 | 			'Report Issue',
353 | 			'Restart Extension'
354 | 		);
355 | 
356 | 		switch (action) {
357 | 			case 'View Details':
358 | 				await this.showErrorDetails(errorDetails);
359 | 				break;
360 | 			case 'Report Issue':
361 | 				await this.openIssueReport(errorDetails);
362 | 				break;
363 | 			case 'Restart Extension':
364 | 				await vscode.commands.executeCommand('workbench.action.reloadWindow');
365 | 				break;
366 | 		}
367 | 	}
368 | 
369 | 	/**
370 | 	 * Add error event listener
371 | 	 */
372 | 	onError(listener: (error: ErrorDetails) => void): void {
373 | 		this.errorListeners.push(listener);
374 | 	}
375 | 
376 | 	/**
377 | 	 * Remove error event listener
378 | 	 */
379 | 	removeErrorListener(listener: (error: ErrorDetails) => void): void {
380 | 		const index = this.errorListeners.indexOf(listener);
381 | 		if (index !== -1) {
382 | 			this.errorListeners.splice(index, 1);
383 | 		}
384 | 	}
385 | 
386 | 	/**
387 | 	 * Get error log
388 | 	 */
389 | 	getErrorLog(
390 | 		category?: ErrorCategory,
391 | 		severity?: ErrorSeverity
392 | 	): ErrorLogEntry[] {
393 | 		let filteredLog = this.errorLog;
394 | 
395 | 		if (category) {
396 | 			filteredLog = filteredLog.filter(
397 | 				(entry) => entry.error.category === category
398 | 			);
399 | 		}
400 | 
401 | 		if (severity) {
402 | 			filteredLog = filteredLog.filter(
403 | 				(entry) => entry.error.severity === severity
404 | 			);
405 | 		}
406 | 
407 | 		return filteredLog.slice().reverse(); // Most recent first
408 | 	}
409 | 
410 | 	/**
411 | 	 * Clear error log
412 | 	 */
413 | 	clearErrorLog(): void {
414 | 		this.errorLog = [];
415 | 	}
416 | 
417 | 	/**
418 | 	 * Export error log for debugging
419 | 	 */
420 | 	exportErrorLog(): string {
421 | 		return JSON.stringify(this.errorLog, null, 2);
422 | 	}
423 | 
424 | 	/**
425 | 	 * Create error details from error instance
426 | 	 */
427 | 	private createErrorDetails(
428 | 		error: Error | TaskMasterError,
429 | 		context?: Record<string, any>
430 | 	): ErrorDetails {
431 | 		if (error instanceof TaskMasterError) {
432 | 			const details = error.toErrorDetails();
433 | 			if (context) {
434 | 				details.context = { ...details.context, ...context };
435 | 			}
436 | 			return details;
437 | 		}
438 | 
439 | 		// Handle standard Error objects
440 | 		return {
441 | 			code: 'UNKNOWN_ERROR',
442 | 			message: error.message || 'An unknown error occurred',
443 | 			category: ErrorCategory.INTERNAL,
444 | 			severity: ErrorSeverity.MEDIUM,
445 | 			timestamp: new Date(),
446 | 			context: { ...context, errorName: error.name },
447 | 			stack: error.stack
448 | 		};
449 | 	}
450 | 
451 | 	/**
452 | 	 * Log error to internal log
453 | 	 */
454 | 	private logError(errorDetails: ErrorDetails): ErrorLogEntry {
455 | 		const logEntry: ErrorLogEntry = {
456 | 			id: `err_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
457 | 			error: errorDetails,
458 | 			resolved: false,
459 | 			attempts: 0
460 | 		};
461 | 
462 | 		this.errorLog.push(logEntry);
463 | 
464 | 		// Maintain log size limit
465 | 		if (this.errorLog.length > this.maxLogSize) {
466 | 			this.errorLog = this.errorLog.slice(-this.maxLogSize);
467 | 		}
468 | 
469 | 		return logEntry;
470 | 	}
471 | 
472 | 	/**
473 | 	 * Mark error as resolved
474 | 	 */
475 | 	private markErrorResolved(errorId: string): void {
476 | 		const entry = this.errorLog.find((e) => e.id === errorId);
477 | 		if (entry) {
478 | 			entry.resolved = true;
479 | 			entry.resolvedAt = new Date();
480 | 		}
481 | 	}
482 | 
483 | 	/**
484 | 	 * Show user notification based on error severity and user preferences
485 | 	 */
486 | 	private async showUserNotification(
487 | 		errorDetails: ErrorDetails
488 | 	): Promise<void> {
489 | 		// Check if user wants to see this notification
490 | 		if (!shouldShowNotification(errorDetails.category, errorDetails.severity)) {
491 | 			return;
492 | 		}
493 | 
494 | 		const notificationType = getNotificationType(
495 | 			errorDetails.category,
496 | 			errorDetails.severity
497 | 		);
498 | 		const message = errorDetails.userAction
499 | 			? `${errorDetails.message} ${errorDetails.userAction}`
500 | 			: errorDetails.message;
501 | 
502 | 		// Handle different notification types based on user preferences
503 | 		switch (notificationType) {
504 | 			case 'VSCODE_ERROR':
505 | 				await vscode.window.showErrorMessage(message);
506 | 				break;
507 | 			case 'VSCODE_WARNING':
508 | 				await vscode.window.showWarningMessage(message);
509 | 				break;
510 | 			case 'VSCODE_INFO':
511 | 				await vscode.window.showInformationMessage(message);
512 | 				break;
513 | 			case 'TOAST_SUCCESS':
514 | 			case 'TOAST_INFO':
515 | 			case 'TOAST_WARNING':
516 | 			case 'TOAST_ERROR':
517 | 				// These will be handled by the webview toast system
518 | 				// The error listener in extension.ts will send these to webview
519 | 				break;
520 | 			case 'CONSOLE_ONLY':
521 | 			case 'SILENT':
522 | 				// No user notification, just console logging
523 | 				break;
524 | 			default:
525 | 				// Fallback to severity-based notifications
526 | 				switch (errorDetails.severity) {
527 | 					case ErrorSeverity.CRITICAL:
528 | 						await vscode.window.showErrorMessage(message);
529 | 						break;
530 | 					case ErrorSeverity.HIGH:
531 | 						await vscode.window.showErrorMessage(message);
532 | 						break;
533 | 					case ErrorSeverity.MEDIUM:
534 | 						await vscode.window.showWarningMessage(message);
535 | 						break;
536 | 					case ErrorSeverity.LOW:
537 | 						await vscode.window.showInformationMessage(message);
538 | 						break;
539 | 				}
540 | 		}
541 | 	}
542 | 
543 | 	/**
544 | 	 * Log to console with appropriate level
545 | 	 */
546 | 	private logToConsole(errorDetails: ErrorDetails): void {
547 | 		const logMessage = `[${errorDetails.category}] ${errorDetails.code}: ${errorDetails.message}`;
548 | 
549 | 		switch (errorDetails.severity) {
550 | 			case ErrorSeverity.CRITICAL:
551 | 			case ErrorSeverity.HIGH:
552 | 				logger.error(logMessage, errorDetails);
553 | 				break;
554 | 			case ErrorSeverity.MEDIUM:
555 | 				logger.warn(logMessage, errorDetails);
556 | 				break;
557 | 			case ErrorSeverity.LOW:
558 | 				console.info(logMessage, errorDetails);
559 | 				break;
560 | 		}
561 | 	}
562 | 
563 | 	/**
564 | 	 * Show detailed error information
565 | 	 */
566 | 	private async showErrorDetails(errorDetails: ErrorDetails): Promise<void> {
567 | 		const details = [
568 | 			`Error Code: ${errorDetails.code}`,
569 | 			`Category: ${errorDetails.category}`,
570 | 			`Severity: ${errorDetails.severity}`,
571 | 			`Time: ${errorDetails.timestamp.toISOString()}`,
572 | 			`Message: ${errorDetails.message}`
573 | 		];
574 | 
575 | 		if (errorDetails.context) {
576 | 			details.push(`Context: ${JSON.stringify(errorDetails.context, null, 2)}`);
577 | 		}
578 | 
579 | 		if (errorDetails.stack) {
580 | 			details.push(`Stack Trace: ${errorDetails.stack}`);
581 | 		}
582 | 
583 | 		const content = details.join('\n\n');
584 | 
585 | 		// Create temporary document to show error details
586 | 		const doc = await vscode.workspace.openTextDocument({
587 | 			content,
588 | 			language: 'plaintext'
589 | 		});
590 | 
591 | 		await vscode.window.showTextDocument(doc);
592 | 	}
593 | 
594 | 	/**
595 | 	 * Open GitHub issue report
596 | 	 */
597 | 	private async openIssueReport(errorDetails: ErrorDetails): Promise<void> {
598 | 		const issueTitle = encodeURIComponent(
599 | 			`Error: ${errorDetails.code} - ${errorDetails.message}`
600 | 		);
601 | 		const issueBody = encodeURIComponent(`
602 | **Error Details:**
603 | - Code: ${errorDetails.code}
604 | - Category: ${errorDetails.category}
605 | - Severity: ${errorDetails.severity}
606 | - Time: ${errorDetails.timestamp.toISOString()}
607 | 
608 | **Message:**
609 | ${errorDetails.message}
610 | 
611 | **Context:**
612 | ${errorDetails.context ? JSON.stringify(errorDetails.context, null, 2) : 'None'}
613 | 
614 | **Steps to Reproduce:**
615 | 1. 
616 | 2. 
617 | 3. 
618 | 
619 | **Expected Behavior:**
620 | 
621 | 
622 | **Additional Notes:**
623 | 
624 |     `);
625 | 
626 | 		const issueUrl = `https://github.com/eyaltoledano/claude-task-master/issues/new?title=${issueTitle}&body=${issueBody}`;
627 | 		await vscode.env.openExternal(vscode.Uri.parse(issueUrl));
628 | 	}
629 | 
630 | 	/**
631 | 	 * Notify error listeners
632 | 	 */
633 | 	private notifyErrorListeners(errorDetails: ErrorDetails): void {
634 | 		this.errorListeners.forEach((listener) => {
635 | 			try {
636 | 				listener(errorDetails);
637 | 			} catch (error) {
638 | 				logger.error('Error in error listener:', error);
639 | 			}
640 | 		});
641 | 	}
642 | 
643 | 	/**
644 | 	 * Setup global error handlers
645 | 	 */
646 | 	private setupGlobalErrorHandlers(): void {
647 | 		// Handle unhandled promise rejections
648 | 		process.on('unhandledRejection', (reason, promise) => {
649 | 			// Create a concrete error class for internal errors
650 | 			class InternalError extends TaskMasterError {
651 | 				constructor(
652 | 					message: string,
653 | 					code: string,
654 | 					severity: ErrorSeverity,
655 | 					context?: Record<string, any>
656 | 				) {
657 | 					super(message, code, ErrorCategory.INTERNAL, severity, context);
658 | 				}
659 | 			}
660 | 
661 | 			const error = new InternalError(
662 | 				'Unhandled Promise Rejection',
663 | 				'UNHANDLED_REJECTION',
664 | 				ErrorSeverity.HIGH,
665 | 				{ reason: String(reason), promise: String(promise) }
666 | 			);
667 | 			this.handleError(error);
668 | 		});
669 | 
670 | 		// Handle uncaught exceptions
671 | 		process.on('uncaughtException', (error) => {
672 | 			// Create a concrete error class for internal errors
673 | 			class InternalError extends TaskMasterError {
674 | 				constructor(
675 | 					message: string,
676 | 					code: string,
677 | 					severity: ErrorSeverity,
678 | 					context?: Record<string, any>
679 | 				) {
680 | 					super(message, code, ErrorCategory.INTERNAL, severity, context);
681 | 				}
682 | 			}
683 | 
684 | 			const taskMasterError = new InternalError(
685 | 				'Uncaught Exception',
686 | 				'UNCAUGHT_EXCEPTION',
687 | 				ErrorSeverity.CRITICAL,
688 | 				{ originalError: error.message, stack: error.stack }
689 | 			);
690 | 			this.handleCriticalError(taskMasterError);
691 | 		});
692 | 	}
693 | }
694 | 
695 | /**
696 |  * Utility functions for error handling
697 |  */
698 | export function getErrorHandler(): ErrorHandler {
699 | 	return ErrorHandler.getInstance();
700 | }
701 | 
702 | export function createRecoveryAction(
703 | 	action: () => Promise<void>,
704 | 	description: string
705 | ) {
706 | 	return {
707 | 		automatic: false,
708 | 		action,
709 | 		description
710 | 	};
711 | }
712 | 
713 | export function createAutoRecoveryAction(
714 | 	action: () => Promise<void>,
715 | 	description: string
716 | ) {
717 | 	return {
718 | 		automatic: true,
719 | 		action,
720 | 		description
721 | 	};
722 | }
723 | 
724 | // Default error categorization rules
725 | export const ERROR_CATEGORIZATION_RULES: Record<string, ErrorCategory> = {
726 | 	// Network patterns
727 | 	ECONNREFUSED: ErrorCategory.NETWORK,
728 | 	ENOTFOUND: ErrorCategory.NETWORK,
729 | 	ETIMEDOUT: ErrorCategory.NETWORK,
730 | 	'Network request failed': ErrorCategory.NETWORK,
731 | 	'fetch failed': ErrorCategory.NETWORK,
732 | 
733 | 	// MCP patterns
734 | 	MCP: ErrorCategory.MCP_CONNECTION,
735 | 	'Task Master': ErrorCategory.TASK_MASTER_API,
736 | 	polling: ErrorCategory.TASK_MASTER_API,
737 | 
738 | 	// VS Code patterns
739 | 	vscode: ErrorCategory.VSCODE_API,
740 | 	webview: ErrorCategory.WEBVIEW,
741 | 	extension: ErrorCategory.EXTENSION_HOST,
742 | 
743 | 	// Data patterns
744 | 	JSON: ErrorCategory.DATA_PARSING,
745 | 	parse: ErrorCategory.DATA_PARSING,
746 | 	validation: ErrorCategory.DATA_VALIDATION,
747 | 	invalid: ErrorCategory.DATA_VALIDATION,
748 | 
749 | 	// Permission patterns
750 | 	EACCES: ErrorCategory.PERMISSION,
751 | 	EPERM: ErrorCategory.PERMISSION,
752 | 	permission: ErrorCategory.PERMISSION,
753 | 
754 | 	// File system patterns
755 | 	ENOENT: ErrorCategory.FILE_SYSTEM,
756 | 	EISDIR: ErrorCategory.FILE_SYSTEM,
757 | 	file: ErrorCategory.FILE_SYSTEM
758 | };
759 | 
760 | // Severity mapping based on error categories
761 | export const CATEGORY_SEVERITY_MAPPING: Record<ErrorCategory, ErrorSeverity> = {
762 | 	[ErrorCategory.NETWORK]: ErrorSeverity.MEDIUM,
763 | 	[ErrorCategory.MCP_CONNECTION]: ErrorSeverity.HIGH,
764 | 	[ErrorCategory.TASK_MASTER_API]: ErrorSeverity.HIGH,
765 | 	[ErrorCategory.DATA_VALIDATION]: ErrorSeverity.MEDIUM,
766 | 	[ErrorCategory.DATA_PARSING]: ErrorSeverity.HIGH,
767 | 	[ErrorCategory.TASK_DATA_CORRUPTION]: ErrorSeverity.CRITICAL,
768 | 	[ErrorCategory.VSCODE_API]: ErrorSeverity.HIGH,
769 | 	[ErrorCategory.WEBVIEW]: ErrorSeverity.MEDIUM,
770 | 	[ErrorCategory.EXTENSION_HOST]: ErrorSeverity.CRITICAL,
771 | 	[ErrorCategory.USER_INTERACTION]: ErrorSeverity.LOW,
772 | 	[ErrorCategory.DRAG_DROP]: ErrorSeverity.MEDIUM,
773 | 	[ErrorCategory.COMPONENT_RENDER]: ErrorSeverity.MEDIUM,
774 | 	[ErrorCategory.PERMISSION]: ErrorSeverity.CRITICAL,
775 | 	[ErrorCategory.FILE_SYSTEM]: ErrorSeverity.HIGH,
776 | 	[ErrorCategory.CONFIGURATION]: ErrorSeverity.MEDIUM,
777 | 	[ErrorCategory.UNKNOWN]: ErrorSeverity.HIGH,
778 | 	// Legacy mappings for existing categories
779 | 	[ErrorCategory.TASK_LOADING]: ErrorSeverity.HIGH,
780 | 	[ErrorCategory.UI_RENDERING]: ErrorSeverity.MEDIUM,
781 | 	[ErrorCategory.VALIDATION]: ErrorSeverity.MEDIUM,
782 | 	[ErrorCategory.INTERNAL]: ErrorSeverity.HIGH
783 | };
784 | 
785 | // Notification type mapping based on severity
786 | export const SEVERITY_NOTIFICATION_MAPPING: Record<
787 | 	ErrorSeverity,
788 | 	NotificationType
789 | > = {
790 | 	[ErrorSeverity.LOW]: NotificationType.TOAST_INFO,
791 | 	[ErrorSeverity.MEDIUM]: NotificationType.TOAST_WARNING,
792 | 	[ErrorSeverity.HIGH]: NotificationType.VSCODE_WARNING,
793 | 	[ErrorSeverity.CRITICAL]: NotificationType.VSCODE_ERROR
794 | };
795 | 
796 | /**
797 |  * Automatically categorize an error based on its message and type
798 |  */
799 | export function categorizeError(
800 | 	error: Error | unknown,
801 | 	operation?: string
802 | ): ErrorCategory {
803 | 	const errorMessage = error instanceof Error ? error.message : String(error);
804 | 	const errorStack = error instanceof Error ? error.stack : undefined;
805 | 	const searchText =
806 | 		`${errorMessage} ${errorStack || ''} ${operation || ''}`.toLowerCase();
807 | 
808 | 	for (const [pattern, category] of Object.entries(
809 | 		ERROR_CATEGORIZATION_RULES
810 | 	)) {
811 | 		if (searchText.includes(pattern.toLowerCase())) {
812 | 			return category;
813 | 		}
814 | 	}
815 | 
816 | 	return ErrorCategory.UNKNOWN;
817 | }
818 | 
819 | export function getSuggestedSeverity(category: ErrorCategory): ErrorSeverity {
820 | 	return CATEGORY_SEVERITY_MAPPING[category] || ErrorSeverity.HIGH;
821 | }
822 | 
823 | export function getSuggestedNotificationType(
824 | 	severity: ErrorSeverity
825 | ): NotificationType {
826 | 	return (
827 | 		SEVERITY_NOTIFICATION_MAPPING[severity] || NotificationType.CONSOLE_ONLY
828 | 	);
829 | }
830 | 
831 | export function createErrorContext(
832 | 	error: Error | unknown,
833 | 	operation?: string,
834 | 	overrides?: Partial<ErrorContext>
835 | ): ErrorContext {
836 | 	const category = categorizeError(error, operation);
837 | 	const severity = getSuggestedSeverity(category);
838 | 	const notificationType = getSuggestedNotificationType(severity);
839 | 
840 | 	const baseContext: ErrorContext = {
841 | 		category,
842 | 		severity,
843 | 		message: error instanceof Error ? error.message : String(error),
844 | 		originalError: error,
845 | 		operation,
846 | 		timestamp: Date.now(),
847 | 		stackTrace: error instanceof Error ? error.stack : undefined,
848 | 		isRecoverable: severity !== ErrorSeverity.CRITICAL,
849 | 		notificationType,
850 | 		showToUser:
851 | 			severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL,
852 | 		logToConsole: true,
853 | 		logToFile:
854 | 			severity === ErrorSeverity.HIGH || severity === ErrorSeverity.CRITICAL
855 | 	};
856 | 
857 | 	return { ...baseContext, ...overrides };
858 | }
859 | 
```

--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/update-task-by-id.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | 
  3 | jest.unstable_mockModule('fs', () => {
  4 | 	const mockFs = {
  5 | 		existsSync: jest.fn(() => true),
  6 | 		writeFileSync: jest.fn(),
  7 | 		readFileSync: jest.fn(),
  8 | 		unlinkSync: jest.fn()
  9 | 	};
 10 | 	return { default: mockFs, ...mockFs };
 11 | });
 12 | 
 13 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
 14 | 	readJSON: jest.fn(),
 15 | 	writeJSON: jest.fn(),
 16 | 	log: jest.fn(),
 17 | 	isSilentMode: jest.fn(() => false),
 18 | 	findProjectRoot: jest.fn(() => '/project'),
 19 | 	flattenTasksWithSubtasks: jest.fn(() => []),
 20 | 	truncate: jest.fn((t) => t),
 21 | 	isEmpty: jest.fn(() => false),
 22 | 	resolveEnvVariable: jest.fn(),
 23 | 	findTaskById: jest.fn(),
 24 | 	getCurrentTag: jest.fn(() => 'master'),
 25 | 	resolveTag: jest.fn(() => 'master'),
 26 | 	addComplexityToTask: jest.fn((task, complexity) => ({ ...task, complexity })),
 27 | 	getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || []),
 28 | 	setTasksForTag: jest.fn(),
 29 | 	ensureTagMetadata: jest.fn((tagObj) => tagObj)
 30 | }));
 31 | 
 32 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
 33 | 	displayBanner: jest.fn(),
 34 | 	getStatusWithColor: jest.fn((s) => s),
 35 | 	startLoadingIndicator: jest.fn(() => ({ stop: jest.fn() })),
 36 | 	stopLoadingIndicator: jest.fn(),
 37 | 	succeedLoadingIndicator: jest.fn(),
 38 | 	failLoadingIndicator: jest.fn(),
 39 | 	warnLoadingIndicator: jest.fn(),
 40 | 	infoLoadingIndicator: jest.fn(),
 41 | 	displayAiUsageSummary: jest.fn(),
 42 | 	displayContextAnalysis: jest.fn()
 43 | }));
 44 | 
 45 | jest.unstable_mockModule(
 46 | 	'../../../../../scripts/modules/task-manager/generate-task-files.js',
 47 | 	() => ({
 48 | 		default: jest.fn().mockResolvedValue()
 49 | 	})
 50 | );
 51 | 
 52 | jest.unstable_mockModule(
 53 | 	'../../../../../scripts/modules/ai-services-unified.js',
 54 | 	() => ({
 55 | 		generateTextService: jest
 56 | 			.fn()
 57 | 			.mockResolvedValue({ mainResult: { content: '{}' }, telemetryData: {} }),
 58 | 		generateObjectService: jest.fn().mockResolvedValue({
 59 | 			mainResult: {
 60 | 				task: {
 61 | 					id: 1,
 62 | 					title: 'Updated Task',
 63 | 					description: 'Updated description',
 64 | 					status: 'pending',
 65 | 					dependencies: [],
 66 | 					priority: 'medium',
 67 | 					details: null,
 68 | 					testStrategy: null,
 69 | 					subtasks: []
 70 | 				}
 71 | 			},
 72 | 			telemetryData: {}
 73 | 		})
 74 | 	})
 75 | );
 76 | 
 77 | jest.unstable_mockModule(
 78 | 	'../../../../../scripts/modules/config-manager.js',
 79 | 	() => ({
 80 | 		getDebugFlag: jest.fn(() => false),
 81 | 		isApiKeySet: jest.fn(() => true),
 82 | 		hasCodebaseAnalysis: jest.fn(() => false)
 83 | 	})
 84 | );
 85 | 
 86 | // Mock @tm/bridge module
 87 | jest.unstable_mockModule('@tm/bridge', () => ({
 88 | 	tryUpdateViaRemote: jest.fn().mockResolvedValue(null)
 89 | }));
 90 | 
 91 | // Mock bridge-utils module
 92 | jest.unstable_mockModule(
 93 | 	'../../../../../scripts/modules/bridge-utils.js',
 94 | 	() => ({
 95 | 		createBridgeLogger: jest.fn(() => ({
 96 | 			logger: {
 97 | 				info: jest.fn(),
 98 | 				warn: jest.fn(),
 99 | 				error: jest.fn(),
100 | 				debug: jest.fn()
101 | 			},
102 | 			report: jest.fn(),
103 | 			isMCP: false
104 | 		}))
105 | 	})
106 | );
107 | 
108 | // Mock prompt-manager module
109 | jest.unstable_mockModule(
110 | 	'../../../../../scripts/modules/prompt-manager.js',
111 | 	() => ({
112 | 		getPromptManager: jest.fn().mockReturnValue({
113 | 			loadPrompt: jest.fn((promptId, params) => ({
114 | 				systemPrompt:
115 | 					'You are an AI assistant that helps update a software development task with new requirements and information.',
116 | 				userPrompt: `Update the following task based on the provided information: ${params?.updatePrompt || 'User prompt for task update'}`,
117 | 				metadata: {
118 | 					templateId: 'update-task',
119 | 					version: '1.0.0',
120 | 					variant: 'default',
121 | 					parameters: params || {}
122 | 				}
123 | 			}))
124 | 		})
125 | 	})
126 | );
127 | 
128 | // Mock contextGatherer module
129 | jest.unstable_mockModule(
130 | 	'../../../../../scripts/modules/utils/contextGatherer.js',
131 | 	() => ({
132 | 		ContextGatherer: jest.fn().mockImplementation(() => ({
133 | 			gather: jest.fn().mockResolvedValue({
134 | 				fullContext: '',
135 | 				summary: ''
136 | 			})
137 | 		}))
138 | 	})
139 | );
140 | 
141 | // Mock fuzzyTaskSearch module
142 | jest.unstable_mockModule(
143 | 	'../../../../../scripts/modules/utils/fuzzyTaskSearch.js',
144 | 	() => ({
145 | 		FuzzyTaskSearch: jest.fn().mockImplementation(() => ({
146 | 			search: jest.fn().mockReturnValue([]),
147 | 			findRelevantTasks: jest.fn().mockReturnValue([]),
148 | 			getTaskIds: jest.fn().mockReturnValue([])
149 | 		}))
150 | 	})
151 | );
152 | 
153 | const { readJSON, log } = await import(
154 | 	'../../../../../scripts/modules/utils.js'
155 | );
156 | const { tryUpdateViaRemote } = await import('@tm/bridge');
157 | const { createBridgeLogger } = await import(
158 | 	'../../../../../scripts/modules/bridge-utils.js'
159 | );
160 | const { getPromptManager } = await import(
161 | 	'../../../../../scripts/modules/prompt-manager.js'
162 | );
163 | const { ContextGatherer } = await import(
164 | 	'../../../../../scripts/modules/utils/contextGatherer.js'
165 | );
166 | const { FuzzyTaskSearch } = await import(
167 | 	'../../../../../scripts/modules/utils/fuzzyTaskSearch.js'
168 | );
169 | const { default: updateTaskById } = await import(
170 | 	'../../../../../scripts/modules/task-manager/update-task-by-id.js'
171 | );
172 | 
173 | // Import test fixtures for consistent sample data
174 | import {
175 | 	taggedEmptyTasks,
176 | 	taggedOneTask
177 | } from '../../../../fixtures/sample-tasks.js';
178 | 
179 | describe('updateTaskById validation', () => {
180 | 	beforeEach(() => {
181 | 		jest.clearAllMocks();
182 | 		jest.spyOn(process, 'exit').mockImplementation(() => {
183 | 			throw new Error('process.exit called');
184 | 		});
185 | 	});
186 | 
187 | 	test('throws error if prompt is empty', async () => {
188 | 		await expect(
189 | 			updateTaskById(
190 | 				'tasks/tasks.json',
191 | 				1,
192 | 				'',
193 | 				false,
194 | 				{ tag: 'master' },
195 | 				'json'
196 | 			)
197 | 		).rejects.toThrow('Prompt cannot be empty');
198 | 	});
199 | 
200 | 	test('throws error if task file missing', async () => {
201 | 		const fs = await import('fs');
202 | 		fs.existsSync.mockReturnValue(false);
203 | 		await expect(
204 | 			updateTaskById(
205 | 				'tasks/tasks.json',
206 | 				1,
207 | 				'prompt',
208 | 				false,
209 | 				{
210 | 					tag: 'master'
211 | 				},
212 | 				'json'
213 | 			)
214 | 		).rejects.toThrow('Tasks file not found');
215 | 	});
216 | 
217 | 	test('throws error when task ID not found', async () => {
218 | 		const fs = await import('fs');
219 | 		fs.existsSync.mockReturnValue(true);
220 | 		readJSON.mockReturnValue(taggedEmptyTasks);
221 | 		await expect(
222 | 			updateTaskById(
223 | 				'tasks/tasks.json',
224 | 				42,
225 | 				'prompt',
226 | 				false,
227 | 				{
228 | 					tag: 'master'
229 | 				},
230 | 				'json'
231 | 			)
232 | 		).rejects.toThrow('Task with ID 42 not found');
233 | 		// Note: The error is reported through the bridge logger (report),
234 | 		// not the log function, so we don't assert on log being called
235 | 	});
236 | });
237 | 
238 | describe('updateTaskById success path with generateObjectService', () => {
239 | 	let fs;
240 | 	let generateObjectService;
241 | 
242 | 	beforeEach(async () => {
243 | 		jest.clearAllMocks();
244 | 		jest.spyOn(process, 'exit').mockImplementation(() => {
245 | 			throw new Error('process.exit called');
246 | 		});
247 | 		fs = await import('fs');
248 | 		const aiServices = await import(
249 | 			'../../../../../scripts/modules/ai-services-unified.js'
250 | 		);
251 | 		generateObjectService = aiServices.generateObjectService;
252 | 	});
253 | 
254 | 	test('successfully updates task with all fields from generateObjectService', async () => {
255 | 		fs.existsSync.mockReturnValue(true);
256 | 		readJSON.mockReturnValue({
257 | 			tag: 'master',
258 | 			tasks: [
259 | 				{
260 | 					id: 1,
261 | 					title: 'Original Task',
262 | 					description: 'Original description',
263 | 					status: 'pending',
264 | 					dependencies: [],
265 | 					priority: 'low',
266 | 					details: null,
267 | 					testStrategy: null,
268 | 					subtasks: []
269 | 				}
270 | 			]
271 | 		});
272 | 
273 | 		const updatedTaskData = {
274 | 			id: 1,
275 | 			title: 'Updated Task',
276 | 			description: 'Updated description',
277 | 			status: 'pending',
278 | 			dependencies: [2],
279 | 			priority: 'high',
280 | 			details: 'New implementation details',
281 | 			testStrategy: 'Unit tests required',
282 | 			subtasks: [
283 | 				{
284 | 					id: 1,
285 | 					title: 'Subtask 1',
286 | 					description: 'First subtask',
287 | 					status: 'pending',
288 | 					dependencies: []
289 | 				}
290 | 			]
291 | 		};
292 | 
293 | 		generateObjectService.mockResolvedValue({
294 | 			mainResult: {
295 | 				task: updatedTaskData
296 | 			},
297 | 			telemetryData: {
298 | 				model: 'claude-3-5-sonnet-20241022',
299 | 				inputTokens: 100,
300 | 				outputTokens: 200
301 | 			}
302 | 		});
303 | 
304 | 		const result = await updateTaskById(
305 | 			'tasks/tasks.json',
306 | 			1,
307 | 			'Update task with new requirements',
308 | 			false,
309 | 			{ tag: 'master' },
310 | 			'json'
311 | 		);
312 | 
313 | 		// Verify generateObjectService was called (not generateTextService)
314 | 		expect(generateObjectService).toHaveBeenCalled();
315 | 		const callArgs = generateObjectService.mock.calls[0][0];
316 | 
317 | 		// Verify correct arguments were passed
318 | 		expect(callArgs).toMatchObject({
319 | 			role: 'main',
320 | 			commandName: 'update-task',
321 | 			objectName: 'task'
322 | 		});
323 | 		expect(callArgs.schema).toBeDefined();
324 | 		expect(callArgs.systemPrompt).toContain(
325 | 			'update a software development task'
326 | 		);
327 | 		expect(callArgs.prompt).toContain('Update task with new requirements');
328 | 
329 | 		// Verify the returned task contains all expected fields
330 | 		expect(result).toEqual({
331 | 			updatedTask: expect.objectContaining({
332 | 				id: 1,
333 | 				title: 'Updated Task',
334 | 				description: 'Updated description',
335 | 				status: 'pending',
336 | 				dependencies: [2],
337 | 				priority: 'high',
338 | 				details: 'New implementation details',
339 | 				testStrategy: 'Unit tests required',
340 | 				subtasks: expect.arrayContaining([
341 | 					expect.objectContaining({
342 | 						id: 1,
343 | 						title: 'Subtask 1',
344 | 						description: 'First subtask',
345 | 						status: 'pending'
346 | 					})
347 | 				])
348 | 			}),
349 | 			telemetryData: expect.objectContaining({
350 | 				model: 'claude-3-5-sonnet-20241022',
351 | 				inputTokens: 100,
352 | 				outputTokens: 200
353 | 			}),
354 | 			tagInfo: undefined
355 | 		});
356 | 	});
357 | 
358 | 	test('handles generateObjectService with malformed mainResult', async () => {
359 | 		fs.existsSync.mockReturnValue(true);
360 | 		readJSON.mockReturnValue({
361 | 			tag: 'master',
362 | 			tasks: [
363 | 				{
364 | 					id: 1,
365 | 					title: 'Task',
366 | 					description: 'Description',
367 | 					status: 'pending',
368 | 					dependencies: [],
369 | 					priority: 'medium',
370 | 					details: null,
371 | 					testStrategy: null,
372 | 					subtasks: []
373 | 				}
374 | 			]
375 | 		});
376 | 
377 | 		generateObjectService.mockResolvedValue({
378 | 			mainResult: {
379 | 				task: null // Malformed: task is null
380 | 			},
381 | 			telemetryData: {}
382 | 		});
383 | 
384 | 		await expect(
385 | 			updateTaskById(
386 | 				'tasks/tasks.json',
387 | 				1,
388 | 				'Update task',
389 | 				false,
390 | 				{ tag: 'master' },
391 | 				'json'
392 | 			)
393 | 		).rejects.toThrow('Received invalid task object from AI');
394 | 	});
395 | 
396 | 	test('handles generateObjectService with missing required fields', async () => {
397 | 		fs.existsSync.mockReturnValue(true);
398 | 		readJSON.mockReturnValue({
399 | 			tag: 'master',
400 | 			tasks: [
401 | 				{
402 | 					id: 1,
403 | 					title: 'Task',
404 | 					description: 'Description',
405 | 					status: 'pending',
406 | 					dependencies: [],
407 | 					priority: 'medium',
408 | 					details: null,
409 | 					testStrategy: null,
410 | 					subtasks: []
411 | 				}
412 | 			]
413 | 		});
414 | 
415 | 		generateObjectService.mockResolvedValue({
416 | 			mainResult: {
417 | 				task: {
418 | 					id: 1,
419 | 					// Missing title and description
420 | 					status: 'pending',
421 | 					dependencies: [],
422 | 					priority: 'medium'
423 | 				}
424 | 			},
425 | 			telemetryData: {}
426 | 		});
427 | 
428 | 		await expect(
429 | 			updateTaskById(
430 | 				'tasks/tasks.json',
431 | 				1,
432 | 				'Update task',
433 | 				false,
434 | 				{ tag: 'master' },
435 | 				'json'
436 | 			)
437 | 		).rejects.toThrow('Updated task missing required fields');
438 | 	});
439 | });
440 | 
441 | describe('Remote Update via Bridge', () => {
442 | 	let fs;
443 | 	let generateObjectService;
444 | 
445 | 	beforeEach(async () => {
446 | 		jest.clearAllMocks();
447 | 		jest.spyOn(process, 'exit').mockImplementation(() => {
448 | 			throw new Error('process.exit called');
449 | 		});
450 | 		fs = await import('fs');
451 | 		const aiServices = await import(
452 | 			'../../../../../scripts/modules/ai-services-unified.js'
453 | 		);
454 | 		generateObjectService = aiServices.generateObjectService;
455 | 	});
456 | 
457 | 	test('should use remote update result when tryUpdateViaRemote succeeds', async () => {
458 | 		// Arrange - Mock successful remote update
459 | 		const remoteResult = {
460 | 			success: true,
461 | 			message: 'Task updated successfully via remote',
462 | 			data: {
463 | 				task: {
464 | 					id: 1,
465 | 					title: 'Updated via Remote',
466 | 					description: 'Updated description from remote',
467 | 					status: 'in-progress',
468 | 					dependencies: [],
469 | 					priority: 'high',
470 | 					details: 'Remote update details',
471 | 					testStrategy: 'Remote test strategy',
472 | 					subtasks: []
473 | 				}
474 | 			}
475 | 		};
476 | 		tryUpdateViaRemote.mockResolvedValue(remoteResult);
477 | 
478 | 		fs.existsSync.mockReturnValue(true);
479 | 		readJSON.mockReturnValue({
480 | 			tag: 'master',
481 | 			tasks: [
482 | 				{
483 | 					id: 1,
484 | 					title: 'Original Task',
485 | 					description: 'Original description',
486 | 					status: 'pending',
487 | 					dependencies: [],
488 | 					priority: 'medium',
489 | 					details: 'Original details',
490 | 					testStrategy: 'Original test strategy',
491 | 					subtasks: []
492 | 				}
493 | 			]
494 | 		});
495 | 
496 | 		// Act
497 | 		const result = await updateTaskById(
498 | 			'tasks/tasks.json',
499 | 			1,
500 | 			'Update this task',
501 | 			false,
502 | 			{ tag: 'master' },
503 | 			'json'
504 | 		);
505 | 
506 | 		// Assert - Should use remote result and NOT call local AI service
507 | 		expect(tryUpdateViaRemote).toHaveBeenCalled();
508 | 		expect(generateObjectService).not.toHaveBeenCalled();
509 | 		expect(result).toEqual(remoteResult);
510 | 	});
511 | 
512 | 	test('should fallback to local update when tryUpdateViaRemote returns null', async () => {
513 | 		// Arrange - Mock remote returning null (no remote available)
514 | 		tryUpdateViaRemote.mockResolvedValue(null);
515 | 
516 | 		fs.existsSync.mockReturnValue(true);
517 | 		readJSON.mockReturnValue({
518 | 			tag: 'master',
519 | 			tasks: [
520 | 				{
521 | 					id: 1,
522 | 					title: 'Task',
523 | 					description: 'Description',
524 | 					status: 'pending',
525 | 					dependencies: [],
526 | 					priority: 'medium',
527 | 					details: 'Details',
528 | 					testStrategy: 'Test strategy',
529 | 					subtasks: []
530 | 				}
531 | 			]
532 | 		});
533 | 
534 | 		generateObjectService.mockResolvedValue({
535 | 			mainResult: {
536 | 				task: {
537 | 					id: 1,
538 | 					title: 'Updated Task',
539 | 					description: 'Updated description',
540 | 					status: 'in-progress',
541 | 					dependencies: [],
542 | 					priority: 'high',
543 | 					details: 'Updated details',
544 | 					testStrategy: 'Updated test strategy',
545 | 					subtasks: []
546 | 				}
547 | 			},
548 | 			telemetryData: {}
549 | 		});
550 | 
551 | 		// Act
552 | 		await updateTaskById(
553 | 			'tasks/tasks.json',
554 | 			1,
555 | 			'Update this task',
556 | 			false,
557 | 			{ tag: 'master' },
558 | 			'json'
559 | 		);
560 | 
561 | 		// Assert - Should fallback to local update
562 | 		expect(tryUpdateViaRemote).toHaveBeenCalled();
563 | 		expect(generateObjectService).toHaveBeenCalled();
564 | 	});
565 | 
566 | 	test('should propagate error when tryUpdateViaRemote throws error', async () => {
567 | 		// Arrange - Mock remote throwing error (it re-throws, doesn't return null)
568 | 		tryUpdateViaRemote.mockImplementation(() =>
569 | 			Promise.reject(new Error('Remote update service unavailable'))
570 | 		);
571 | 
572 | 		fs.existsSync.mockReturnValue(true);
573 | 		readJSON.mockReturnValue({
574 | 			tag: 'master',
575 | 			tasks: [
576 | 				{
577 | 					id: 1,
578 | 					title: 'Task',
579 | 					description: 'Description',
580 | 					status: 'pending',
581 | 					dependencies: [],
582 | 					priority: 'medium',
583 | 					details: 'Details',
584 | 					testStrategy: 'Test strategy',
585 | 					subtasks: []
586 | 				}
587 | 			]
588 | 		});
589 | 
590 | 		generateObjectService.mockResolvedValue({
591 | 			mainResult: {
592 | 				task: {
593 | 					id: 1,
594 | 					title: 'Updated Task',
595 | 					description: 'Updated description',
596 | 					status: 'in-progress',
597 | 					dependencies: [],
598 | 					priority: 'high',
599 | 					details: 'Updated details',
600 | 					testStrategy: 'Updated test strategy',
601 | 					subtasks: []
602 | 				}
603 | 			},
604 | 			telemetryData: {}
605 | 		});
606 | 
607 | 		// Act & Assert - Should propagate the error (not fallback to local)
608 | 		await expect(
609 | 			updateTaskById(
610 | 				'tasks/tasks.json',
611 | 				1,
612 | 				'Update this task',
613 | 				false,
614 | 				{ tag: 'master' },
615 | 				'json'
616 | 			)
617 | 		).rejects.toThrow('Remote update service unavailable');
618 | 
619 | 		expect(tryUpdateViaRemote).toHaveBeenCalled();
620 | 		// Local update should NOT be called when remote throws
621 | 		expect(generateObjectService).not.toHaveBeenCalled();
622 | 	});
623 | });
624 | 
625 | describe('Prompt Manager Integration', () => {
626 | 	let fs;
627 | 	let generateObjectService;
628 | 
629 | 	beforeEach(async () => {
630 | 		jest.clearAllMocks();
631 | 		jest.spyOn(process, 'exit').mockImplementation(() => {
632 | 			throw new Error('process.exit called');
633 | 		});
634 | 		fs = await import('fs');
635 | 		const aiServices = await import(
636 | 			'../../../../../scripts/modules/ai-services-unified.js'
637 | 		);
638 | 		generateObjectService = aiServices.generateObjectService;
639 | 		tryUpdateViaRemote.mockResolvedValue(null); // No remote
640 | 	});
641 | 
642 | 	test('should use prompt manager to load update prompts', async () => {
643 | 		// Arrange
644 | 		fs.existsSync.mockReturnValue(true);
645 | 		readJSON.mockReturnValue({
646 | 			tag: 'master',
647 | 			tasks: [
648 | 				{
649 | 					id: 1,
650 | 					title: 'Task',
651 | 					description: 'Description',
652 | 					status: 'pending',
653 | 					dependencies: [],
654 | 					priority: 'medium',
655 | 					details: 'Details',
656 | 					testStrategy: 'Test strategy',
657 | 					subtasks: []
658 | 				}
659 | 			]
660 | 		});
661 | 
662 | 		generateObjectService.mockResolvedValue({
663 | 			mainResult: {
664 | 				task: {
665 | 					id: 1,
666 | 					title: 'Updated Task',
667 | 					description: 'Updated description',
668 | 					status: 'in-progress',
669 | 					dependencies: [],
670 | 					priority: 'high',
671 | 					details: 'Updated details',
672 | 					testStrategy: 'Updated test strategy',
673 | 					subtasks: []
674 | 				}
675 | 			},
676 | 			telemetryData: {}
677 | 		});
678 | 
679 | 		// Act
680 | 		await updateTaskById(
681 | 			'tasks/tasks.json',
682 | 			1,
683 | 			'Update this task with new requirements',
684 | 			false,
685 | 			{ tag: 'master', projectRoot: '/mock/project' },
686 | 			'json'
687 | 		);
688 | 
689 | 		// Assert - Prompt manager should be called
690 | 		expect(getPromptManager).toHaveBeenCalled();
691 | 		const promptManagerInstance = getPromptManager.mock.results[0].value;
692 | 		expect(promptManagerInstance.loadPrompt).toHaveBeenCalled();
693 | 	});
694 | });
695 | 
696 | describe('Context Gathering Integration', () => {
697 | 	let fs;
698 | 	let generateObjectService;
699 | 
700 | 	beforeEach(async () => {
701 | 		jest.clearAllMocks();
702 | 		jest.spyOn(process, 'exit').mockImplementation(() => {
703 | 			throw new Error('process.exit called');
704 | 		});
705 | 		fs = await import('fs');
706 | 		const aiServices = await import(
707 | 			'../../../../../scripts/modules/ai-services-unified.js'
708 | 		);
709 | 		generateObjectService = aiServices.generateObjectService;
710 | 		tryUpdateViaRemote.mockResolvedValue(null); // No remote
711 | 	});
712 | 
713 | 	test('should gather project context when projectRoot is provided', async () => {
714 | 		// Arrange
715 | 		const mockContextGatherer = {
716 | 			gather: jest.fn().mockResolvedValue({
717 | 				fullContext: 'Project context from files',
718 | 				summary: 'Context summary'
719 | 			})
720 | 		};
721 | 		ContextGatherer.mockImplementation(() => mockContextGatherer);
722 | 
723 | 		fs.existsSync.mockReturnValue(true);
724 | 		readJSON.mockReturnValue({
725 | 			tag: 'master',
726 | 			tasks: [
727 | 				{
728 | 					id: 1,
729 | 					title: 'Task',
730 | 					description: 'Description',
731 | 					status: 'pending',
732 | 					dependencies: [],
733 | 					priority: 'medium',
734 | 					details: 'Details',
735 | 					testStrategy: 'Test strategy',
736 | 					subtasks: []
737 | 				}
738 | 			]
739 | 		});
740 | 
741 | 		generateObjectService.mockResolvedValue({
742 | 			mainResult: {
743 | 				task: {
744 | 					id: 1,
745 | 					title: 'Updated Task',
746 | 					description: 'Updated description',
747 | 					status: 'in-progress',
748 | 					dependencies: [],
749 | 					priority: 'high',
750 | 					details: 'Updated details',
751 | 					testStrategy: 'Updated test strategy',
752 | 					subtasks: []
753 | 				}
754 | 			},
755 | 			telemetryData: {}
756 | 		});
757 | 
758 | 		// Act
759 | 		await updateTaskById(
760 | 			'tasks/tasks.json',
761 | 			1,
762 | 			'Update with context',
763 | 			false,
764 | 			{ tag: 'master', projectRoot: '/mock/project' },
765 | 			'json'
766 | 		);
767 | 
768 | 		// Assert - Context gatherer should be instantiated and used
769 | 		expect(ContextGatherer).toHaveBeenCalledWith('/mock/project', 'master');
770 | 		expect(mockContextGatherer.gather).toHaveBeenCalled();
771 | 	});
772 | });
773 | 
774 | describe('Fuzzy Task Search Integration', () => {
775 | 	let fs;
776 | 	let generateObjectService;
777 | 
778 | 	beforeEach(async () => {
779 | 		jest.clearAllMocks();
780 | 		jest.spyOn(process, 'exit').mockImplementation(() => {
781 | 			throw new Error('process.exit called');
782 | 		});
783 | 		fs = await import('fs');
784 | 		const aiServices = await import(
785 | 			'../../../../../scripts/modules/ai-services-unified.js'
786 | 		);
787 | 		generateObjectService = aiServices.generateObjectService;
788 | 		tryUpdateViaRemote.mockResolvedValue(null); // No remote
789 | 	});
790 | 
791 | 	test('should use fuzzy search to find related tasks for context', async () => {
792 | 		// Arrange
793 | 		const mockFuzzySearch = {
794 | 			findRelevantTasks: jest.fn().mockReturnValue([
795 | 				{ id: 2, title: 'Related Task 1', score: 0.9 },
796 | 				{ id: 3, title: 'Related Task 2', score: 0.85 }
797 | 			]),
798 | 			getTaskIds: jest.fn().mockReturnValue(['2', '3'])
799 | 		};
800 | 		FuzzyTaskSearch.mockImplementation(() => mockFuzzySearch);
801 | 
802 | 		fs.existsSync.mockReturnValue(true);
803 | 		readJSON.mockReturnValue({
804 | 			tag: 'master',
805 | 			tasks: [
806 | 				{
807 | 					id: 1,
808 | 					title: 'Task to update',
809 | 					description: 'Description',
810 | 					status: 'pending',
811 | 					dependencies: [],
812 | 					priority: 'medium',
813 | 					details: 'Details',
814 | 					testStrategy: 'Test strategy',
815 | 					subtasks: []
816 | 				},
817 | 				{
818 | 					id: 2,
819 | 					title: 'Related Task 1',
820 | 					description: 'Related description',
821 | 					status: 'done',
822 | 					dependencies: [],
823 | 					priority: 'medium',
824 | 					details: 'Related details',
825 | 					testStrategy: 'Related test strategy',
826 | 					subtasks: []
827 | 				},
828 | 				{
829 | 					id: 3,
830 | 					title: 'Related Task 2',
831 | 					description: 'Another related description',
832 | 					status: 'pending',
833 | 					dependencies: [],
834 | 					priority: 'low',
835 | 					details: 'More details',
836 | 					testStrategy: 'Test strategy',
837 | 					subtasks: []
838 | 				}
839 | 			]
840 | 		});
841 | 
842 | 		generateObjectService.mockResolvedValue({
843 | 			mainResult: {
844 | 				task: {
845 | 					id: 1,
846 | 					title: 'Updated Task',
847 | 					description: 'Updated description',
848 | 					status: 'in-progress',
849 | 					dependencies: [],
850 | 					priority: 'high',
851 | 					details: 'Updated details',
852 | 					testStrategy: 'Updated test strategy',
853 | 					subtasks: []
854 | 				}
855 | 			},
856 | 			telemetryData: {}
857 | 		});
858 | 
859 | 		// Act
860 | 		await updateTaskById(
861 | 			'tasks/tasks.json',
862 | 			1,
863 | 			'Update with related task context',
864 | 			false,
865 | 			{ tag: 'master' },
866 | 			'json'
867 | 		);
868 | 
869 | 		// Assert - Fuzzy search should be instantiated and used
870 | 		expect(FuzzyTaskSearch).toHaveBeenCalled();
871 | 		expect(mockFuzzySearch.findRelevantTasks).toHaveBeenCalledWith(
872 | 			expect.stringContaining('Task to update'),
873 | 			expect.objectContaining({
874 | 				maxResults: 5,
875 | 				includeSelf: true
876 | 			})
877 | 		);
878 | 		expect(mockFuzzySearch.getTaskIds).toHaveBeenCalled();
879 | 	});
880 | });
881 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/tasks/services/task-service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Task Service
  3 |  * Core service for task operations - handles business logic between storage and API
  4 |  */
  5 | 
  6 | import type {
  7 | 	Task,
  8 | 	TaskFilter,
  9 | 	TaskStatus,
 10 | 	StorageType
 11 | } from '../../../common/types/index.js';
 12 | import type { IStorage } from '../../../common/interfaces/storage.interface.js';
 13 | import { ConfigManager } from '../../config/managers/config-manager.js';
 14 | import { StorageFactory } from '../../storage/services/storage-factory.js';
 15 | import { TaskEntity } from '../entities/task.entity.js';
 16 | import { ERROR_CODES, TaskMasterError } from '../../../common/errors/task-master-error.js';
 17 | import { getLogger } from '../../../common/logger/factory.js';
 18 | import type { ExpandTaskResult } from '../../integration/services/task-expansion.service.js';
 19 | 
 20 | /**
 21 |  * Result returned by getTaskList
 22 |  */
 23 | export interface TaskListResult {
 24 | 	/** The filtered list of tasks */
 25 | 	tasks: Task[];
 26 | 	/** Total number of tasks before filtering */
 27 | 	total: number;
 28 | 	/** Number of tasks after filtering */
 29 | 	filtered: number;
 30 | 	/** The tag/brief name for these tasks (brief name for API storage, tag for file storage) */
 31 | 	tag?: string;
 32 | 	/** Storage type being used */
 33 | 	storageType: StorageType;
 34 | }
 35 | 
 36 | /**
 37 |  * Options for getTaskList
 38 |  */
 39 | export interface GetTaskListOptions {
 40 | 	/** Optional tag override (uses active tag from config if not provided) */
 41 | 	tag?: string;
 42 | 	/** Filter criteria */
 43 | 	filter?: TaskFilter;
 44 | 	/** Include subtasks in response */
 45 | 	includeSubtasks?: boolean;
 46 | }
 47 | 
 48 | /**
 49 |  * TaskService handles all task-related operations
 50 |  * This is where business logic lives - it coordinates between ConfigManager and Storage
 51 |  */
 52 | export class TaskService {
 53 | 	private configManager: ConfigManager;
 54 | 	private storage: IStorage;
 55 | 	private initialized = false;
 56 | 	private logger = getLogger('TaskService');
 57 | 
 58 | 	constructor(configManager: ConfigManager) {
 59 | 		this.configManager = configManager;
 60 | 
 61 | 		// Storage will be created during initialization
 62 | 		this.storage = null as any;
 63 | 	}
 64 | 
 65 | 	/**
 66 | 	 * Initialize the service
 67 | 	 */
 68 | 	async initialize(): Promise<void> {
 69 | 		if (this.initialized) return;
 70 | 
 71 | 		// Create storage based on configuration
 72 | 		const storageConfig = this.configManager.getStorageConfig();
 73 | 		const projectRoot = this.configManager.getProjectRoot();
 74 | 
 75 | 		this.storage = await StorageFactory.createFromStorageConfig(
 76 | 			storageConfig,
 77 | 			projectRoot
 78 | 		);
 79 | 
 80 | 		// Initialize storage
 81 | 		await this.storage.initialize();
 82 | 
 83 | 		this.initialized = true;
 84 | 	}
 85 | 
 86 | 	/**
 87 | 	 * Get list of tasks
 88 | 	 * This is the main method that retrieves tasks from storage and applies filters
 89 | 	 */
 90 | 	async getTaskList(options: GetTaskListOptions = {}): Promise<TaskListResult> {
 91 | 		// Determine which tag to use
 92 | 		const activeTag = this.configManager.getActiveTag();
 93 | 		const tag = options.tag || activeTag;
 94 | 
 95 | 		try {
 96 | 			// Determine if we can push filters to storage layer
 97 | 			const canPushStatusFilter =
 98 | 				options.filter?.status &&
 99 | 				!options.filter.priority &&
100 | 				!options.filter.tags &&
101 | 				!options.filter.assignee &&
102 | 				!options.filter.search &&
103 | 				options.filter.hasSubtasks === undefined;
104 | 
105 | 			// Build storage-level options
106 | 			const storageOptions: any = {};
107 | 
108 | 			// Push status filter to storage if it's the only filter
109 | 			if (canPushStatusFilter) {
110 | 				const statuses = Array.isArray(options.filter!.status)
111 | 					? options.filter!.status
112 | 					: [options.filter!.status];
113 | 				// Only push single status to storage (multiple statuses need in-memory filtering)
114 | 				if (statuses.length === 1) {
115 | 					storageOptions.status = statuses[0];
116 | 				}
117 | 			}
118 | 
119 | 			// Push subtask exclusion to storage
120 | 			if (options.includeSubtasks === false) {
121 | 				storageOptions.excludeSubtasks = true;
122 | 			}
123 | 
124 | 			// Load tasks from storage with pushed-down filters
125 | 			const rawTasks = await this.storage.loadTasks(tag, storageOptions);
126 | 
127 | 			// Get total count without status filters, but preserve subtask exclusion
128 | 			const baseOptions: any = {};
129 | 			if (options.includeSubtasks === false) {
130 | 				baseOptions.excludeSubtasks = true;
131 | 			}
132 | 
133 | 			const allTasks =
134 | 				storageOptions.status !== undefined
135 | 					? await this.storage.loadTasks(tag, baseOptions)
136 | 					: rawTasks;
137 | 
138 | 			// Convert to TaskEntity for business logic operations
139 | 			const taskEntities = TaskEntity.fromArray(rawTasks);
140 | 
141 | 			// Apply remaining filters in-memory if needed
142 | 			let filteredEntities = taskEntities;
143 | 			if (options.filter && !canPushStatusFilter) {
144 | 				filteredEntities = this.applyFilters(taskEntities, options.filter);
145 | 			} else if (
146 | 				options.filter?.status &&
147 | 				Array.isArray(options.filter.status) &&
148 | 				options.filter.status.length > 1
149 | 			) {
150 | 				// Multiple statuses - filter in-memory
151 | 				filteredEntities = this.applyFilters(taskEntities, options.filter);
152 | 			}
153 | 
154 | 			// Convert back to plain objects
155 | 			const tasks = filteredEntities.map((entity) => entity.toJSON());
156 | 
157 | 			// For API storage, use brief name. For file storage, use tag.
158 | 			// This way consumers don't need to know about the difference.
159 | 			const storageType = this.getStorageType();
160 | 			const tagOrBrief =
161 | 				storageType === 'api'
162 | 					? this.storage.getCurrentBriefName() || tag
163 | 					: tag;
164 | 
165 | 			return {
166 | 				tasks,
167 | 				total: allTasks.length,
168 | 				filtered: filteredEntities.length,
169 | 				tag: tagOrBrief, // For API: brief name, For file: tag
170 | 				storageType
171 | 			};
172 | 		} catch (error) {
173 | 			// If it's a user-facing error (like NO_BRIEF_SELECTED), don't log it as an internal error
174 | 			if (
175 | 				error instanceof TaskMasterError &&
176 | 				error.is(ERROR_CODES.NO_BRIEF_SELECTED)
177 | 			) {
178 | 				// Just re-throw user-facing errors without wrapping
179 | 				throw error;
180 | 			}
181 | 
182 | 			// Log internal errors
183 | 			this.logger.error('Failed to get task list', error);
184 | 			throw new TaskMasterError(
185 | 				'Failed to get task list',
186 | 				ERROR_CODES.INTERNAL_ERROR,
187 | 				{
188 | 					operation: 'getTaskList',
189 | 					tag,
190 | 					hasFilter: !!options.filter
191 | 				},
192 | 				error as Error
193 | 			);
194 | 		}
195 | 	}
196 | 
197 | 	/**
198 | 	 * Get a single task by ID - delegates to storage layer
199 | 	 */
200 | 	async getTask(taskId: string, tag?: string): Promise<Task | null> {
201 | 		// Use provided tag or get active tag
202 | 		const activeTag = tag || this.getActiveTag();
203 | 
204 | 		try {
205 | 			// Delegate to storage layer which handles the specific logic for tasks vs subtasks
206 | 			return await this.storage.loadTask(String(taskId), activeTag);
207 | 		} catch (error) {
208 | 			// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
209 | 			if (
210 | 				error instanceof TaskMasterError &&
211 | 				error.is(ERROR_CODES.NO_BRIEF_SELECTED)
212 | 			) {
213 | 				throw error;
214 | 			}
215 | 
216 | 			throw new TaskMasterError(
217 | 				`Failed to get task ${taskId}`,
218 | 				ERROR_CODES.STORAGE_ERROR,
219 | 				{
220 | 					operation: 'getTask',
221 | 					resource: 'task',
222 | 					taskId: String(taskId),
223 | 					tag: activeTag
224 | 				},
225 | 				error as Error
226 | 			);
227 | 		}
228 | 	}
229 | 
230 | 	/**
231 | 	 * Get tasks filtered by status
232 | 	 */
233 | 	async getTasksByStatus(
234 | 		status: TaskStatus | TaskStatus[],
235 | 		tag?: string
236 | 	): Promise<Task[]> {
237 | 		const statuses = Array.isArray(status) ? status : [status];
238 | 
239 | 		const result = await this.getTaskList({
240 | 			tag,
241 | 			filter: { status: statuses }
242 | 		});
243 | 
244 | 		return result.tasks;
245 | 	}
246 | 
247 | 	/**
248 | 	 * Get statistics about tasks
249 | 	 */
250 | 	async getTaskStats(tag?: string): Promise<{
251 | 		total: number;
252 | 		byStatus: Record<TaskStatus, number>;
253 | 		withSubtasks: number;
254 | 		blocked: number;
255 | 		storageType: StorageType;
256 | 	}> {
257 | 		const result = await this.getTaskList({
258 | 			tag,
259 | 			includeSubtasks: true
260 | 		});
261 | 
262 | 		const stats = {
263 | 			total: result.total,
264 | 			byStatus: {} as Record<TaskStatus, number>,
265 | 			withSubtasks: 0,
266 | 			blocked: 0,
267 | 			storageType: result.storageType
268 | 		};
269 | 
270 | 		// Initialize all statuses
271 | 		const allStatuses: TaskStatus[] = [
272 | 			'pending',
273 | 			'in-progress',
274 | 			'done',
275 | 			'deferred',
276 | 			'cancelled',
277 | 			'blocked',
278 | 			'review'
279 | 		];
280 | 
281 | 		allStatuses.forEach((status) => {
282 | 			stats.byStatus[status] = 0;
283 | 		});
284 | 
285 | 		// Count tasks
286 | 		result.tasks.forEach((task) => {
287 | 			stats.byStatus[task.status]++;
288 | 
289 | 			if (task.subtasks && task.subtasks.length > 0) {
290 | 				stats.withSubtasks++;
291 | 			}
292 | 
293 | 			if (task.status === 'blocked') {
294 | 				stats.blocked++;
295 | 			}
296 | 		});
297 | 
298 | 		return stats;
299 | 	}
300 | 
301 | 	/**
302 | 	 * Get next available task to work on
303 | 	 * Prioritizes eligible subtasks from in-progress parent tasks before falling back to top-level tasks
304 | 	 */
305 | 	async getNextTask(tag?: string): Promise<Task | null> {
306 | 		const result = await this.getTaskList({
307 | 			tag,
308 | 			filter: {
309 | 				status: ['pending', 'in-progress', 'done']
310 | 			}
311 | 		});
312 | 
313 | 		const allTasks = result.tasks;
314 | 		const priorityValues = { critical: 4, high: 3, medium: 2, low: 1 };
315 | 
316 | 		// Helper to convert subtask dependencies to full dotted notation
317 | 		const toFullSubId = (
318 | 			parentId: string,
319 | 			maybeDotId: string | number
320 | 		): string => {
321 | 			if (typeof maybeDotId === 'string' && maybeDotId.includes('.')) {
322 | 				return maybeDotId;
323 | 			}
324 | 			return `${parentId}.${maybeDotId}`;
325 | 		};
326 | 
327 | 		// Build completed IDs set (both tasks and subtasks)
328 | 		const completedIds = new Set<string>();
329 | 		allTasks.forEach((t) => {
330 | 			if (t.status === 'done') {
331 | 				completedIds.add(String(t.id));
332 | 			}
333 | 			if (Array.isArray(t.subtasks)) {
334 | 				t.subtasks.forEach((st) => {
335 | 					if (st.status === 'done') {
336 | 						completedIds.add(`${t.id}.${st.id}`);
337 | 					}
338 | 				});
339 | 			}
340 | 		});
341 | 
342 | 		// 1) Look for eligible subtasks from in-progress parent tasks
343 | 		const candidateSubtasks: Array<Task & { parentId?: string }> = [];
344 | 
345 | 		allTasks
346 | 			.filter((t) => t.status === 'in-progress' && Array.isArray(t.subtasks))
347 | 			.forEach((parent) => {
348 | 				parent.subtasks!.forEach((st) => {
349 | 					const stStatus = (st.status || 'pending').toLowerCase();
350 | 					if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
351 | 
352 | 					const fullDeps =
353 | 						st.dependencies?.map((d) => toFullSubId(String(parent.id), d)) ??
354 | 						[];
355 | 					const depsSatisfied =
356 | 						fullDeps.length === 0 ||
357 | 						fullDeps.every((depId) => completedIds.has(String(depId)));
358 | 
359 | 					if (depsSatisfied) {
360 | 						candidateSubtasks.push({
361 | 							id: `${parent.id}.${st.id}`,
362 | 							title: st.title || `Subtask ${st.id}`,
363 | 							status: st.status || 'pending',
364 | 							priority: st.priority || parent.priority || 'medium',
365 | 							dependencies: fullDeps,
366 | 							parentId: String(parent.id),
367 | 							description: st.description,
368 | 							details: st.details,
369 | 							testStrategy: st.testStrategy,
370 | 							subtasks: []
371 | 						} as Task & { parentId: string });
372 | 					}
373 | 				});
374 | 			});
375 | 
376 | 		if (candidateSubtasks.length > 0) {
377 | 			// Sort by priority → dependency count → parent ID → subtask ID
378 | 			candidateSubtasks.sort((a, b) => {
379 | 				const pa =
380 | 					priorityValues[a.priority as keyof typeof priorityValues] ?? 2;
381 | 				const pb =
382 | 					priorityValues[b.priority as keyof typeof priorityValues] ?? 2;
383 | 				if (pb !== pa) return pb - pa;
384 | 
385 | 				if (a.dependencies!.length !== b.dependencies!.length) {
386 | 					return a.dependencies!.length - b.dependencies!.length;
387 | 				}
388 | 
389 | 				// Compare parent then subtask ID numerically
390 | 				const [aPar, aSub] = String(a.id).split('.').map(Number);
391 | 				const [bPar, bSub] = String(b.id).split('.').map(Number);
392 | 				if (aPar !== bPar) return aPar - bPar;
393 | 				return aSub - bSub;
394 | 			});
395 | 
396 | 			return candidateSubtasks[0];
397 | 		}
398 | 
399 | 		// 2) Fall back to top-level tasks (original logic)
400 | 		const eligibleTasks = allTasks.filter((task) => {
401 | 			const status = (task.status || 'pending').toLowerCase();
402 | 			if (status !== 'pending' && status !== 'in-progress') return false;
403 | 
404 | 			const deps = task.dependencies ?? [];
405 | 			return deps.every((depId) => completedIds.has(String(depId)));
406 | 		});
407 | 
408 | 		if (eligibleTasks.length === 0) return null;
409 | 
410 | 		// Sort by priority → dependency count → task ID
411 | 		const nextTask = eligibleTasks.sort((a, b) => {
412 | 			const pa = priorityValues[a.priority as keyof typeof priorityValues] ?? 2;
413 | 			const pb = priorityValues[b.priority as keyof typeof priorityValues] ?? 2;
414 | 			if (pb !== pa) return pb - pa;
415 | 
416 | 			const da = (a.dependencies ?? []).length;
417 | 			const db = (b.dependencies ?? []).length;
418 | 			if (da !== db) return da - db;
419 | 
420 | 			return Number(a.id) - Number(b.id);
421 | 		})[0];
422 | 
423 | 		return nextTask;
424 | 	}
425 | 
426 | 	/**
427 | 	 * Apply filters to task entities
428 | 	 */
429 | 	private applyFilters(tasks: TaskEntity[], filter: TaskFilter): TaskEntity[] {
430 | 		return tasks.filter((task) => {
431 | 			// Status filter
432 | 			if (filter.status) {
433 | 				const statuses = Array.isArray(filter.status)
434 | 					? filter.status
435 | 					: [filter.status];
436 | 				if (!statuses.includes(task.status)) {
437 | 					return false;
438 | 				}
439 | 			}
440 | 
441 | 			// Priority filter
442 | 			if (filter.priority) {
443 | 				const priorities = Array.isArray(filter.priority)
444 | 					? filter.priority
445 | 					: [filter.priority];
446 | 				if (!priorities.includes(task.priority)) {
447 | 					return false;
448 | 				}
449 | 			}
450 | 
451 | 			// Tags filter
452 | 			if (filter.tags && filter.tags.length > 0) {
453 | 				if (
454 | 					!task.tags ||
455 | 					!filter.tags.some((tag) => task.tags?.includes(tag))
456 | 				) {
457 | 					return false;
458 | 				}
459 | 			}
460 | 
461 | 			// Assignee filter
462 | 			if (filter.assignee) {
463 | 				if (task.assignee !== filter.assignee) {
464 | 					return false;
465 | 				}
466 | 			}
467 | 
468 | 			// Search filter
469 | 			if (filter.search) {
470 | 				const searchLower = filter.search.toLowerCase();
471 | 				const inTitle = task.title.toLowerCase().includes(searchLower);
472 | 				const inDescription = task.description
473 | 					.toLowerCase()
474 | 					.includes(searchLower);
475 | 				const inDetails = task.details.toLowerCase().includes(searchLower);
476 | 
477 | 				if (!inTitle && !inDescription && !inDetails) {
478 | 					return false;
479 | 				}
480 | 			}
481 | 
482 | 			// Has subtasks filter
483 | 			if (filter.hasSubtasks !== undefined) {
484 | 				const hasSubtasks = task.subtasks.length > 0;
485 | 				if (hasSubtasks !== filter.hasSubtasks) {
486 | 					return false;
487 | 				}
488 | 			}
489 | 
490 | 			return true;
491 | 		});
492 | 	}
493 | 
494 | 	/**
495 | 	 * Get current storage type (resolved at runtime)
496 | 	 * Returns the actual storage type being used, never 'auto'
497 | 	 */
498 | 	getStorageType(): 'file' | 'api' {
499 | 		// Storage interface guarantees this method exists
500 | 		return this.storage.getStorageType();
501 | 	}
502 | 
503 | 	/**
504 | 	 * Get the storage instance
505 | 	 * Internal use only - used by other services in the tasks module
506 | 	 */
507 | 	getStorage(): IStorage {
508 | 		return this.storage;
509 | 	}
510 | 
511 | 	/**
512 | 	 * Get current active tag
513 | 	 */
514 | 	getActiveTag(): string {
515 | 		return this.configManager.getActiveTag();
516 | 	}
517 | 
518 | 	/**
519 | 	 * Set active tag
520 | 	 */
521 | 	async setActiveTag(tag: string): Promise<void> {
522 | 		await this.configManager.setActiveTag(tag);
523 | 	}
524 | 
525 | 	/**
526 | 	 * Update a task with new data (direct structural update)
527 | 	 * @param taskId - Task ID (supports numeric, alphanumeric, and subtask IDs)
528 | 	 * @param updates - Partial task object with fields to update
529 | 	 * @param tag - Optional tag context
530 | 	 */
531 | 	async updateTask(
532 | 		taskId: string | number,
533 | 		updates: Partial<Task>,
534 | 		tag?: string
535 | 	): Promise<void> {
536 | 		// Ensure we have storage
537 | 		if (!this.storage) {
538 | 			throw new TaskMasterError(
539 | 				'Storage not initialized',
540 | 				ERROR_CODES.STORAGE_ERROR
541 | 			);
542 | 		}
543 | 
544 | 		// Auto-initialize if needed
545 | 		if (!this.initialized) {
546 | 			await this.initialize();
547 | 		}
548 | 
549 | 		// Use provided tag or get active tag
550 | 		const activeTag = tag || this.getActiveTag();
551 | 		const taskIdStr = String(taskId);
552 | 
553 | 		try {
554 | 			// Direct update - no AI processing
555 | 			await this.storage.updateTask(taskIdStr, updates, activeTag);
556 | 		} catch (error) {
557 | 			// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
558 | 			if (
559 | 				error instanceof TaskMasterError &&
560 | 				error.is(ERROR_CODES.NO_BRIEF_SELECTED)
561 | 			) {
562 | 				throw error;
563 | 			}
564 | 
565 | 			throw new TaskMasterError(
566 | 				`Failed to update task ${taskId}`,
567 | 				ERROR_CODES.STORAGE_ERROR,
568 | 				{
569 | 					operation: 'updateTask',
570 | 					resource: 'task',
571 | 					taskId: taskIdStr,
572 | 					tag: activeTag
573 | 				},
574 | 				error as Error
575 | 			);
576 | 		}
577 | 	}
578 | 
579 | 	/**
580 | 	 * Update a task using AI-powered prompt (natural language update)
581 | 	 * @param taskId - Task ID (supports numeric, alphanumeric, and subtask IDs)
582 | 	 * @param prompt - Natural language prompt describing the update
583 | 	 * @param tag - Optional tag context
584 | 	 * @param options - Optional update options
585 | 	 * @param options.useResearch - Use research AI for file storage updates
586 | 	 * @param options.mode - Update mode for API storage: 'append', 'update', or 'rewrite'
587 | 	 */
588 | 	async updateTaskWithPrompt(
589 | 		taskId: string | number,
590 | 		prompt: string,
591 | 		tag?: string,
592 | 		options?: { mode?: 'append' | 'update' | 'rewrite'; useResearch?: boolean }
593 | 	): Promise<void> {
594 | 		// Ensure we have storage
595 | 		if (!this.storage) {
596 | 			throw new TaskMasterError(
597 | 				'Storage not initialized',
598 | 				ERROR_CODES.STORAGE_ERROR
599 | 			);
600 | 		}
601 | 
602 | 		// Auto-initialize if needed
603 | 		if (!this.initialized) {
604 | 			await this.initialize();
605 | 		}
606 | 
607 | 		// Use provided tag or get active tag
608 | 		const activeTag = tag || this.getActiveTag();
609 | 		const taskIdStr = String(taskId);
610 | 
611 | 		try {
612 | 			// AI-powered update - send prompt to storage layer
613 | 			// API storage: sends prompt to backend for server-side AI processing
614 | 			// File storage: must use client-side AI logic before calling this
615 | 			await this.storage.updateTaskWithPrompt(
616 | 				taskIdStr,
617 | 				prompt,
618 | 				activeTag,
619 | 				options
620 | 			);
621 | 		} catch (error) {
622 | 			// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
623 | 			if (
624 | 				error instanceof TaskMasterError
625 | 			) {
626 | 				throw error;
627 | 			}
628 | 
629 | 			throw new TaskMasterError(
630 | 				`Failed to update task ${taskId} with prompt`,
631 | 				ERROR_CODES.STORAGE_ERROR,
632 | 				{
633 | 					operation: 'updateTaskWithPrompt',
634 | 					resource: 'task',
635 | 					taskId: taskIdStr,
636 | 					tag: activeTag,
637 | 					promptLength: prompt.length
638 | 				},
639 | 				error as Error
640 | 			);
641 | 		}
642 | 	}
643 | 
644 | 	/**
645 | 	 * Expand a task into subtasks using AI-powered generation
646 | 	 * @param taskId - Task ID to expand (supports numeric and alphanumeric IDs)
647 | 	 * @param tag - Optional tag context
648 | 	 * @param options - Optional expansion options
649 | 	 * @param options.numSubtasks - Number of subtasks to generate
650 | 	 * @param options.useResearch - Use research AI for generation
651 | 	 * @param options.additionalContext - Additional context for generation
652 | 	 * @param options.force - Force regeneration even if subtasks exist
653 | 	 * @returns ExpandTaskResult when using API storage, void for file storage
654 | 	 */
655 | 	async expandTaskWithPrompt(
656 | 		taskId: string | number,
657 | 		tag?: string,
658 | 		options?: {
659 | 			numSubtasks?: number;
660 | 			useResearch?: boolean;
661 | 			additionalContext?: string;
662 | 			force?: boolean;
663 | 		}
664 | 	): Promise<ExpandTaskResult | void> {
665 | 		// Ensure we have storage
666 | 		if (!this.storage) {
667 | 			throw new TaskMasterError(
668 | 				'Storage not initialized',
669 | 				ERROR_CODES.STORAGE_ERROR
670 | 			);
671 | 		}
672 | 
673 | 		// Auto-initialize if needed
674 | 		if (!this.initialized) {
675 | 			await this.initialize();
676 | 		}
677 | 
678 | 		// Use provided tag or get active tag
679 | 		const activeTag = tag || this.getActiveTag();
680 | 		const taskIdStr = String(taskId);
681 | 
682 | 		try {
683 | 			// AI-powered expansion - send to storage layer
684 | 			// API storage: sends request to backend for server-side AI processing
685 | 			// File storage: must use client-side AI logic before calling this
686 | 			return await this.storage.expandTaskWithPrompt(
687 | 				taskIdStr,
688 | 				activeTag,
689 | 				options
690 | 			);
691 | 		} catch (error) {
692 | 			// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
693 | 			if (
694 | 				error instanceof TaskMasterError
695 | 			) {
696 | 				throw error;
697 | 			}
698 | 
699 | 			throw new TaskMasterError(
700 | 				`Failed to expand task ${taskId}`,
701 | 				ERROR_CODES.STORAGE_ERROR,
702 | 				{
703 | 					operation: 'expandTaskWithPrompt',
704 | 					resource: 'task',
705 | 					taskId: taskIdStr,
706 | 					tag: activeTag,
707 | 					numSubtasks: options?.numSubtasks
708 | 				},
709 | 				error as Error
710 | 			);
711 | 		}
712 | 	}
713 | 
714 | 	/**
715 | 	 * Update task status - delegates to storage layer which handles storage-specific logic
716 | 	 */
717 | 	async updateTaskStatus(
718 | 		taskId: string | number,
719 | 		newStatus: TaskStatus,
720 | 		tag?: string
721 | 	): Promise<{
722 | 		success: boolean;
723 | 		oldStatus: TaskStatus;
724 | 		newStatus: TaskStatus;
725 | 		taskId: string;
726 | 	}> {
727 | 		// Ensure we have storage
728 | 		if (!this.storage) {
729 | 			throw new TaskMasterError(
730 | 				'Storage not initialized',
731 | 				ERROR_CODES.STORAGE_ERROR
732 | 			);
733 | 		}
734 | 
735 | 		// Use provided tag or get active tag
736 | 		const activeTag = tag || this.getActiveTag();
737 | 		const taskIdStr = String(taskId);
738 | 
739 | 		try {
740 | 			// Delegate to storage layer which handles the specific logic for tasks vs subtasks
741 | 			return await this.storage.updateTaskStatus(
742 | 				taskIdStr,
743 | 				newStatus,
744 | 				activeTag
745 | 			);
746 | 		} catch (error) {
747 | 			// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
748 | 			if (
749 | 				error instanceof TaskMasterError &&
750 | 				error.is(ERROR_CODES.NO_BRIEF_SELECTED)
751 | 			) {
752 | 				throw error;
753 | 			}
754 | 
755 | 			throw new TaskMasterError(
756 | 				`Failed to update task status for ${taskIdStr}`,
757 | 				ERROR_CODES.STORAGE_ERROR,
758 | 				{
759 | 					operation: 'updateTaskStatus',
760 | 					resource: 'task',
761 | 					taskId: taskIdStr,
762 | 					newStatus,
763 | 					tag: activeTag
764 | 				},
765 | 				error as Error
766 | 			);
767 | 		}
768 | 	}
769 | 
770 | 	/**
771 | 	 * Get all tags with detailed statistics including task counts
772 | 	 * Delegates to storage layer which handles file vs API implementation
773 | 	 */
774 | 	async getTagsWithStats() {
775 | 		// Ensure we have storage
776 | 		if (!this.storage) {
777 | 			throw new TaskMasterError(
778 | 				'Storage not initialized',
779 | 				ERROR_CODES.STORAGE_ERROR
780 | 			);
781 | 		}
782 | 
783 | 		// Auto-initialize if needed
784 | 		if (!this.initialized) {
785 | 			await this.initialize();
786 | 		}
787 | 
788 | 		try {
789 | 			return await this.storage.getTagsWithStats();
790 | 		} catch (error) {
791 | 			// If it's a user-facing error (like NO_BRIEF_SELECTED), don't wrap it
792 | 			if (
793 | 				error instanceof TaskMasterError &&
794 | 				error.is(ERROR_CODES.NO_BRIEF_SELECTED)
795 | 			) {
796 | 				throw error;
797 | 			}
798 | 
799 | 			throw new TaskMasterError(
800 | 				'Failed to get tags with stats',
801 | 				ERROR_CODES.STORAGE_ERROR,
802 | 				{
803 | 					operation: 'getTagsWithStats',
804 | 					resource: 'tags'
805 | 				},
806 | 				error as Error
807 | 			);
808 | 		}
809 | 	}
810 | }
811 | 
```
Page 44/69FirstPrevNextLast