#
tokens: 48667/50000 8/975 files (page 35/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 35 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

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

```typescript
  1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
  2 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
  3 | import * as vscode from 'vscode';
  4 | import * as path from 'path';
  5 | import { logger } from './logger';
  6 | 
  7 | export interface MCPConfig {
  8 | 	command: string;
  9 | 	args: string[];
 10 | 	cwd?: string;
 11 | 	env?: Record<string, string>;
 12 | 	timeout?: number;
 13 | }
 14 | 
 15 | export interface MCPServerStatus {
 16 | 	isRunning: boolean;
 17 | 	pid?: number;
 18 | 	error?: string;
 19 | }
 20 | 
 21 | export class MCPClientManager {
 22 | 	private client: Client | null = null;
 23 | 	private transport: StdioClientTransport | null = null;
 24 | 	private config: MCPConfig;
 25 | 	private status: MCPServerStatus = { isRunning: false };
 26 | 	private connectionPromise: Promise<void> | null = null;
 27 | 
 28 | 	constructor(config: MCPConfig) {
 29 | 		logger.log(
 30 | 			'🔍 DEBUGGING: MCPClientManager constructor called with config:',
 31 | 			config
 32 | 		);
 33 | 		this.config = config;
 34 | 	}
 35 | 
 36 | 	/**
 37 | 	 * Get the current server status
 38 | 	 */
 39 | 	getStatus(): MCPServerStatus {
 40 | 		return { ...this.status };
 41 | 	}
 42 | 
 43 | 	/**
 44 | 	 * Start the MCP server process and establish client connection
 45 | 	 */
 46 | 	async connect(): Promise<void> {
 47 | 		if (this.connectionPromise) {
 48 | 			return this.connectionPromise;
 49 | 		}
 50 | 
 51 | 		this.connectionPromise = this._doConnect();
 52 | 		return this.connectionPromise;
 53 | 	}
 54 | 
 55 | 	private async _doConnect(): Promise<void> {
 56 | 		try {
 57 | 			// Clean up any existing connections
 58 | 			await this.disconnect();
 59 | 
 60 | 			// Create the transport - it will handle spawning the server process internally
 61 | 			logger.log(
 62 | 				`Starting MCP server: ${this.config.command} ${this.config.args?.join(' ') || ''}`
 63 | 			);
 64 | 			logger.log('🔍 DEBUGGING: Transport config cwd:', this.config.cwd);
 65 | 			logger.log('🔍 DEBUGGING: Process cwd before spawn:', process.cwd());
 66 | 
 67 | 			// Test if the target directory and .taskmaster exist
 68 | 			const fs = require('fs');
 69 | 			const path = require('path');
 70 | 			try {
 71 | 				const targetDir = this.config.cwd;
 72 | 				const taskmasterDir = path.join(targetDir, '.taskmaster');
 73 | 				const tasksFile = path.join(taskmasterDir, 'tasks', 'tasks.json');
 74 | 
 75 | 				logger.log(
 76 | 					'🔍 DEBUGGING: Checking target directory:',
 77 | 					targetDir,
 78 | 					'exists:',
 79 | 					fs.existsSync(targetDir)
 80 | 				);
 81 | 				logger.log(
 82 | 					'🔍 DEBUGGING: Checking .taskmaster dir:',
 83 | 					taskmasterDir,
 84 | 					'exists:',
 85 | 					fs.existsSync(taskmasterDir)
 86 | 				);
 87 | 				logger.log(
 88 | 					'🔍 DEBUGGING: Checking tasks.json:',
 89 | 					tasksFile,
 90 | 					'exists:',
 91 | 					fs.existsSync(tasksFile)
 92 | 				);
 93 | 
 94 | 				if (fs.existsSync(tasksFile)) {
 95 | 					const stats = fs.statSync(tasksFile);
 96 | 					logger.log('🔍 DEBUGGING: tasks.json size:', stats.size, 'bytes');
 97 | 				}
 98 | 			} catch (error) {
 99 | 				logger.log('🔍 DEBUGGING: Error checking filesystem:', error);
100 | 			}
101 | 
102 | 			this.transport = new StdioClientTransport({
103 | 				command: this.config.command,
104 | 				args: this.config.args || [],
105 | 				cwd: this.config.cwd,
106 | 				env: {
107 | 					...(Object.fromEntries(
108 | 						Object.entries(process.env).filter(([, v]) => v !== undefined)
109 | 					) as Record<string, string>),
110 | 					...this.config.env
111 | 				}
112 | 			});
113 | 
114 | 			logger.log('🔍 DEBUGGING: Transport created, checking process...');
115 | 
116 | 			// Set up transport event handlers
117 | 			this.transport.onerror = (error: Error) => {
118 | 				logger.error('❌ MCP transport error:', error);
119 | 				logger.error('Transport error details:', {
120 | 					message: error.message,
121 | 					stack: error.stack,
122 | 					code: (error as any).code,
123 | 					errno: (error as any).errno,
124 | 					syscall: (error as any).syscall
125 | 				});
126 | 				this.status = { isRunning: false, error: error.message };
127 | 				vscode.window.showErrorMessage(
128 | 					`TaskMaster MCP transport error: ${error.message}`
129 | 				);
130 | 			};
131 | 
132 | 			this.transport.onclose = () => {
133 | 				logger.log('🔌 MCP transport closed');
134 | 				this.status = { isRunning: false };
135 | 				this.client = null;
136 | 				this.transport = null;
137 | 			};
138 | 
139 | 			// Add message handler like the working debug script
140 | 			this.transport.onmessage = (message: any) => {
141 | 				logger.log('📤 MCP server message:', message);
142 | 			};
143 | 
144 | 			// Create the client
145 | 			this.client = new Client(
146 | 				{
147 | 					name: 'task-master-vscode-extension',
148 | 					version: '1.0.0'
149 | 				},
150 | 				{
151 | 					capabilities: {
152 | 						tools: {}
153 | 					}
154 | 				}
155 | 			);
156 | 
157 | 			// Connect the client to the transport (this automatically starts the transport)
158 | 			logger.log('🔄 Attempting MCP client connection...');
159 | 			logger.log('MCP config:', {
160 | 				command: this.config.command,
161 | 				args: this.config.args,
162 | 				cwd: this.config.cwd
163 | 			});
164 | 			logger.log('Current working directory:', process.cwd());
165 | 			logger.log(
166 | 				'VS Code workspace folders:',
167 | 				vscode.workspace.workspaceFolders?.map((f) => f.uri.fsPath)
168 | 			);
169 | 
170 | 			// Check if process was created before connecting
171 | 			if (this.transport && (this.transport as any).process) {
172 | 				const proc = (this.transport as any).process;
173 | 				logger.log('📝 MCP server process PID:', proc.pid);
174 | 				logger.log('📝 Process working directory will be:', this.config.cwd);
175 | 
176 | 				proc.on('exit', (code: number, signal: string) => {
177 | 					logger.log(
178 | 						`🔚 MCP server process exited with code ${code}, signal ${signal}`
179 | 					);
180 | 					if (code !== 0) {
181 | 						logger.log('❌ Non-zero exit code indicates server failure');
182 | 					}
183 | 				});
184 | 
185 | 				proc.on('error', (error: Error) => {
186 | 					logger.log('❌ MCP server process error:', error);
187 | 				});
188 | 
189 | 				// Listen to stderr to see server-side errors
190 | 				if (proc.stderr) {
191 | 					proc.stderr.on('data', (data: Buffer) => {
192 | 						logger.log('📥 MCP server stderr:', data.toString());
193 | 					});
194 | 				}
195 | 
196 | 				// Listen to stdout for server messages
197 | 				if (proc.stdout) {
198 | 					proc.stdout.on('data', (data: Buffer) => {
199 | 						logger.log('📤 MCP server stdout:', data.toString());
200 | 					});
201 | 				}
202 | 			} else {
203 | 				logger.log('⚠️ No process found in transport before connection');
204 | 			}
205 | 
206 | 			await this.client.connect(this.transport);
207 | 
208 | 			// Update status
209 | 			this.status = {
210 | 				isRunning: true,
211 | 				pid: this.transport.pid || undefined
212 | 			};
213 | 
214 | 			logger.log('MCP client connected successfully');
215 | 
216 | 			// Log Task Master version information after successful connection
217 | 			try {
218 | 				const versionResult = await this.callTool('get_tasks', {});
219 | 				if (versionResult?.content?.[0]?.text) {
220 | 					const response = JSON.parse(versionResult.content[0].text);
221 | 					if (response?.version) {
222 | 						logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
223 | 						logger.log('✅ Task Master MCP Server Connected');
224 | 						logger.log(`   Version: ${response.version.version || 'unknown'}`);
225 | 						logger.log(
226 | 							`   Package: ${response.version.name || 'task-master-ai'}`
227 | 						);
228 | 						if (response.tag) {
229 | 							logger.log(
230 | 								`   Current Tag: ${response.tag.currentTag || 'master'}`
231 | 							);
232 | 						}
233 | 						logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
234 | 					}
235 | 				}
236 | 			} catch (versionError) {
237 | 				logger.log('Note: Could not retrieve Task Master version information');
238 | 			}
239 | 		} catch (error) {
240 | 			logger.error('Failed to connect to MCP server:', error);
241 | 			this.status = {
242 | 				isRunning: false,
243 | 				error: error instanceof Error ? error.message : 'Unknown error'
244 | 			};
245 | 
246 | 			// Clean up on error
247 | 			await this.disconnect();
248 | 
249 | 			throw error;
250 | 		} finally {
251 | 			this.connectionPromise = null;
252 | 		}
253 | 	}
254 | 
255 | 	/**
256 | 	 * Disconnect from the MCP server and clean up resources
257 | 	 */
258 | 	async disconnect(): Promise<void> {
259 | 		logger.log('Disconnecting from MCP server');
260 | 
261 | 		if (this.client) {
262 | 			try {
263 | 				await this.client.close();
264 | 			} catch (error) {
265 | 				logger.error('Error closing MCP client:', error);
266 | 			}
267 | 			this.client = null;
268 | 		}
269 | 
270 | 		if (this.transport) {
271 | 			try {
272 | 				await this.transport.close();
273 | 			} catch (error) {
274 | 				logger.error('Error closing MCP transport:', error);
275 | 			}
276 | 			this.transport = null;
277 | 		}
278 | 
279 | 		this.status = { isRunning: false };
280 | 	}
281 | 
282 | 	/**
283 | 	 * Get the MCP client instance (if connected)
284 | 	 */
285 | 	getClient(): Client | null {
286 | 		return this.client;
287 | 	}
288 | 
289 | 	/**
290 | 	 * Call an MCP tool
291 | 	 */
292 | 	async callTool(
293 | 		toolName: string,
294 | 		arguments_: Record<string, unknown>
295 | 	): Promise<any> {
296 | 		if (!this.client) {
297 | 			throw new Error('MCP client is not connected');
298 | 		}
299 | 
300 | 		try {
301 | 			// Use the configured timeout or default to 5 minutes
302 | 			const timeout = this.config.timeout || 300000; // 5 minutes default
303 | 
304 | 			logger.log(`Calling MCP tool "${toolName}" with timeout: ${timeout}ms`);
305 | 
306 | 			const result = await this.client.callTool(
307 | 				{
308 | 					name: toolName,
309 | 					arguments: arguments_
310 | 				},
311 | 				undefined,
312 | 				{
313 | 					timeout: timeout
314 | 				}
315 | 			);
316 | 
317 | 			return result;
318 | 		} catch (error) {
319 | 			logger.error(`Error calling MCP tool "${toolName}":`, error);
320 | 			throw error;
321 | 		}
322 | 	}
323 | 
324 | 	/**
325 | 	 * Test the connection by calling a simple MCP tool
326 | 	 */
327 | 	async testConnection(): Promise<boolean> {
328 | 		try {
329 | 			// Try to list available tools as a connection test
330 | 			if (!this.client) {
331 | 				return false;
332 | 			}
333 | 
334 | 			// listTools is a simple metadata request, no need for extended timeout
335 | 			const result = await this.client.listTools();
336 | 			logger.log(
337 | 				'Available MCP tools:',
338 | 				result.tools?.map((t) => t.name) || []
339 | 			);
340 | 
341 | 			// Try to get version information by calling a simple tool
342 | 			// The get_tasks tool is lightweight and returns version info
343 | 			try {
344 | 				const versionResult = await this.callTool('get_tasks', {});
345 | 				if (versionResult?.content?.[0]?.text) {
346 | 					// Parse the response to extract version info
347 | 					const response = JSON.parse(versionResult.content[0].text);
348 | 					if (response?.version) {
349 | 						logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
350 | 						logger.log('📦 Task Master MCP Server Connected');
351 | 						logger.log(`   Version: ${response.version.version || 'unknown'}`);
352 | 						logger.log(
353 | 							`   Package: ${response.version.name || 'task-master-ai'}`
354 | 						);
355 | 						if (response.tag) {
356 | 							logger.log(
357 | 								`   Current Tag: ${response.tag.currentTag || 'master'}`
358 | 							);
359 | 						}
360 | 						logger.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
361 | 					}
362 | 				}
363 | 			} catch (versionError) {
364 | 				// Don't fail the connection test if we can't get version info
365 | 				logger.log('Could not retrieve Task Master version information');
366 | 			}
367 | 
368 | 			return true;
369 | 		} catch (error) {
370 | 			logger.error('Connection test failed:', error);
371 | 			return false;
372 | 		}
373 | 	}
374 | 
375 | 	/**
376 | 	 * Get stderr stream from the transport (if available)
377 | 	 */
378 | 	getStderr(): NodeJS.ReadableStream | null {
379 | 		const stderr = this.transport?.stderr;
380 | 		return stderr ? (stderr as unknown as NodeJS.ReadableStream) : null;
381 | 	}
382 | 
383 | 	/**
384 | 	 * Get the process ID of the spawned server
385 | 	 */
386 | 	getPid(): number | null {
387 | 		return this.transport?.pid || null;
388 | 	}
389 | }
390 | 
391 | /**
392 |  * Create MCP configuration from VS Code settings
393 |  */
394 | export function createMCPConfigFromSettings(): MCPConfig {
395 | 	logger.log(
396 | 		'🔍 DEBUGGING: createMCPConfigFromSettings called at',
397 | 		new Date().toISOString()
398 | 	);
399 | 	const config = vscode.workspace.getConfiguration('taskmaster');
400 | 
401 | 	let command = config.get<string>('mcp.command', 'node');
402 | 	let args = config.get<string[]>('mcp.args', []);
403 | 
404 | 	// If using default settings, use the bundled MCP server
405 | 	if (command === 'node' && args.length === 0) {
406 | 		try {
407 | 			// Try to resolve the bundled MCP server
408 | 			const taskMasterPath = require.resolve('task-master-ai');
409 | 			const mcpServerPath = path.resolve(
410 | 				path.dirname(taskMasterPath),
411 | 				'./dist/mcp-server.js'
412 | 			);
413 | 
414 | 			// Verify the server file exists
415 | 			const fs = require('fs');
416 | 			if (!fs.existsSync(mcpServerPath)) {
417 | 				throw new Error('MCP server file not found at: ' + mcpServerPath);
418 | 			}
419 | 
420 | 			args = [mcpServerPath];
421 | 			logger.log(`📦 Using bundled MCP server at: ${mcpServerPath}`);
422 | 		} catch (error) {
423 | 			logger.error('❌ Could not find bundled task-master-ai server:', error);
424 | 			// Fallback to npx
425 | 			command = 'npx';
426 | 			args = ['-y', 'task-master-ai'];
427 | 		}
428 | 	}
429 | 
430 | 	// Use proper VS Code workspace detection
431 | 	const defaultCwd =
432 | 		vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || process.cwd();
433 | 	const cwd = config.get<string>('mcp.cwd', defaultCwd);
434 | 	const env = config.get<Record<string, string>>('mcp.env');
435 | 	const timeout = config.get<number>('mcp.requestTimeoutMs', 300000);
436 | 
437 | 	logger.log('✅ Using workspace directory:', defaultCwd);
438 | 
439 | 	// If using default 'npx', try to find the full path on macOS/Linux
440 | 	if (command === 'npx') {
441 | 		const fs = require('fs');
442 | 		const npxPaths = [
443 | 			'/opt/homebrew/bin/npx', // Homebrew on Apple Silicon
444 | 			'/usr/local/bin/npx', // Homebrew on Intel
445 | 			'/usr/bin/npx', // System npm
446 | 			'npx' // Final fallback to PATH
447 | 		];
448 | 
449 | 		for (const path of npxPaths) {
450 | 			try {
451 | 				if (path === 'npx' || fs.existsSync(path)) {
452 | 					command = path;
453 | 					logger.log(`✅ Using npx at: ${path}`);
454 | 					break;
455 | 				}
456 | 			} catch (error) {
457 | 				// Continue to next path
458 | 			}
459 | 		}
460 | 	}
461 | 
462 | 	return {
463 | 		command,
464 | 		args,
465 | 		cwd: cwd || defaultCwd,
466 | 		env,
467 | 		timeout
468 | 	};
469 | }
470 | 
```

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

```typescript
  1 | import * as vscode from 'vscode';
  2 | import { logger } from './logger';
  3 | import type { MCPConfig } from './mcpClient';
  4 | 
  5 | export interface TaskMasterConfig {
  6 | 	mcp: MCPServerConfig;
  7 | 	ui: UIConfig;
  8 | 	performance: PerformanceConfig;
  9 | 	debug: DebugConfig;
 10 | }
 11 | 
 12 | export interface MCPServerConfig {
 13 | 	command: string;
 14 | 	args: string[];
 15 | 	cwd?: string;
 16 | 	env?: Record<string, string>;
 17 | 	timeout: number;
 18 | 	maxReconnectAttempts: number;
 19 | 	reconnectBackoffMs: number;
 20 | 	maxBackoffMs: number;
 21 | 	healthCheckIntervalMs: number;
 22 | }
 23 | 
 24 | export interface UIConfig {
 25 | 	autoRefresh: boolean;
 26 | 	refreshIntervalMs: number;
 27 | 	theme: 'auto' | 'light' | 'dark';
 28 | 	showCompletedTasks: boolean;
 29 | 	taskDisplayLimit: number;
 30 | 	showPriority: boolean;
 31 | 	showTaskIds: boolean;
 32 | }
 33 | 
 34 | export interface PerformanceConfig {
 35 | 	maxConcurrentRequests: number;
 36 | 	requestTimeoutMs: number;
 37 | 	cacheTasksMs: number;
 38 | 	lazyLoadThreshold: number;
 39 | }
 40 | 
 41 | export interface DebugConfig {
 42 | 	enableLogging: boolean;
 43 | 	logLevel: 'error' | 'warn' | 'info' | 'debug';
 44 | 	enableConnectionMetrics: boolean;
 45 | 	saveEventLogs: boolean;
 46 | 	maxEventLogSize: number;
 47 | }
 48 | 
 49 | export interface ConfigValidationResult {
 50 | 	isValid: boolean;
 51 | 	errors: string[];
 52 | 	warnings: string[];
 53 | }
 54 | 
 55 | export class ConfigManager {
 56 | 	private static instance: ConfigManager | null = null;
 57 | 	private config: TaskMasterConfig;
 58 | 	private configListeners: ((config: TaskMasterConfig) => void)[] = [];
 59 | 
 60 | 	private constructor() {
 61 | 		this.config = this.loadConfig();
 62 | 		this.setupConfigWatcher();
 63 | 	}
 64 | 
 65 | 	/**
 66 | 	 * Get singleton instance
 67 | 	 */
 68 | 	static getInstance(): ConfigManager {
 69 | 		if (!ConfigManager.instance) {
 70 | 			ConfigManager.instance = new ConfigManager();
 71 | 		}
 72 | 		return ConfigManager.instance;
 73 | 	}
 74 | 
 75 | 	/**
 76 | 	 * Get current configuration
 77 | 	 */
 78 | 	getConfig(): TaskMasterConfig {
 79 | 		return { ...this.config };
 80 | 	}
 81 | 
 82 | 	/**
 83 | 	 * Get MCP configuration for the client
 84 | 	 */
 85 | 	getMCPConfig(): MCPConfig {
 86 | 		const mcpConfig = this.config.mcp;
 87 | 		return {
 88 | 			command: mcpConfig.command,
 89 | 			args: mcpConfig.args,
 90 | 			cwd: mcpConfig.cwd,
 91 | 			env: mcpConfig.env
 92 | 		};
 93 | 	}
 94 | 
 95 | 	/**
 96 | 	 * Update configuration (programmatically)
 97 | 	 */
 98 | 	async updateConfig(updates: Partial<TaskMasterConfig>): Promise<void> {
 99 | 		const newConfig = this.mergeConfig(this.config, updates);
100 | 		const validation = this.validateConfig(newConfig);
101 | 
102 | 		if (!validation.isValid) {
103 | 			throw new Error(
104 | 				`Configuration validation failed: ${validation.errors.join(', ')}`
105 | 			);
106 | 		}
107 | 
108 | 		// Update VS Code settings
109 | 		const vsConfig = vscode.workspace.getConfiguration('taskmaster');
110 | 
111 | 		if (updates.mcp) {
112 | 			if (updates.mcp.command !== undefined) {
113 | 				await vsConfig.update(
114 | 					'mcp.command',
115 | 					updates.mcp.command,
116 | 					vscode.ConfigurationTarget.Workspace
117 | 				);
118 | 			}
119 | 			if (updates.mcp.args !== undefined) {
120 | 				await vsConfig.update(
121 | 					'mcp.args',
122 | 					updates.mcp.args,
123 | 					vscode.ConfigurationTarget.Workspace
124 | 				);
125 | 			}
126 | 			if (updates.mcp.cwd !== undefined) {
127 | 				await vsConfig.update(
128 | 					'mcp.cwd',
129 | 					updates.mcp.cwd,
130 | 					vscode.ConfigurationTarget.Workspace
131 | 				);
132 | 			}
133 | 			if (updates.mcp.timeout !== undefined) {
134 | 				await vsConfig.update(
135 | 					'mcp.timeout',
136 | 					updates.mcp.timeout,
137 | 					vscode.ConfigurationTarget.Workspace
138 | 				);
139 | 			}
140 | 		}
141 | 
142 | 		if (updates.ui) {
143 | 			if (updates.ui.autoRefresh !== undefined) {
144 | 				await vsConfig.update(
145 | 					'ui.autoRefresh',
146 | 					updates.ui.autoRefresh,
147 | 					vscode.ConfigurationTarget.Workspace
148 | 				);
149 | 			}
150 | 			if (updates.ui.theme !== undefined) {
151 | 				await vsConfig.update(
152 | 					'ui.theme',
153 | 					updates.ui.theme,
154 | 					vscode.ConfigurationTarget.Workspace
155 | 				);
156 | 			}
157 | 		}
158 | 
159 | 		if (updates.debug) {
160 | 			if (updates.debug.enableLogging !== undefined) {
161 | 				await vsConfig.update(
162 | 					'debug.enableLogging',
163 | 					updates.debug.enableLogging,
164 | 					vscode.ConfigurationTarget.Workspace
165 | 				);
166 | 			}
167 | 			if (updates.debug.logLevel !== undefined) {
168 | 				await vsConfig.update(
169 | 					'debug.logLevel',
170 | 					updates.debug.logLevel,
171 | 					vscode.ConfigurationTarget.Workspace
172 | 				);
173 | 			}
174 | 		}
175 | 
176 | 		this.config = newConfig;
177 | 		this.notifyConfigChange();
178 | 	}
179 | 
180 | 	/**
181 | 	 * Validate configuration
182 | 	 */
183 | 	validateConfig(config: TaskMasterConfig): ConfigValidationResult {
184 | 		const errors: string[] = [];
185 | 		const warnings: string[] = [];
186 | 
187 | 		// Validate MCP configuration
188 | 		if (!config.mcp.command || config.mcp.command.trim() === '') {
189 | 			errors.push('MCP command cannot be empty');
190 | 		}
191 | 
192 | 		if (config.mcp.timeout < 1000) {
193 | 			warnings.push(
194 | 				'MCP timeout is very low (< 1s), this may cause connection issues'
195 | 			);
196 | 		} else if (config.mcp.timeout > 60000) {
197 | 			warnings.push(
198 | 				'MCP timeout is very high (> 60s), this may cause slow responses'
199 | 			);
200 | 		}
201 | 
202 | 		if (config.mcp.maxReconnectAttempts < 1) {
203 | 			errors.push('Max reconnect attempts must be at least 1');
204 | 		} else if (config.mcp.maxReconnectAttempts > 10) {
205 | 			warnings.push(
206 | 				'Max reconnect attempts is very high, this may cause long delays'
207 | 			);
208 | 		}
209 | 
210 | 		// Validate UI configuration
211 | 		if (config.ui.refreshIntervalMs < 1000) {
212 | 			warnings.push(
213 | 				'UI refresh interval is very low (< 1s), this may impact performance'
214 | 			);
215 | 		}
216 | 
217 | 		if (config.ui.taskDisplayLimit < 1) {
218 | 			errors.push('Task display limit must be at least 1');
219 | 		} else if (config.ui.taskDisplayLimit > 1000) {
220 | 			warnings.push(
221 | 				'Task display limit is very high, this may impact performance'
222 | 			);
223 | 		}
224 | 
225 | 		// Validate performance configuration
226 | 		if (config.performance.maxConcurrentRequests < 1) {
227 | 			errors.push('Max concurrent requests must be at least 1');
228 | 		} else if (config.performance.maxConcurrentRequests > 20) {
229 | 			warnings.push(
230 | 				'Max concurrent requests is very high, this may overwhelm the server'
231 | 			);
232 | 		}
233 | 
234 | 		if (config.performance.requestTimeoutMs < 1000) {
235 | 			warnings.push(
236 | 				'Request timeout is very low (< 1s), this may cause premature timeouts'
237 | 			);
238 | 		}
239 | 
240 | 		// Validate debug configuration
241 | 		if (config.debug.maxEventLogSize < 10) {
242 | 			errors.push('Max event log size must be at least 10');
243 | 		} else if (config.debug.maxEventLogSize > 10000) {
244 | 			warnings.push(
245 | 				'Max event log size is very high, this may consume significant memory'
246 | 			);
247 | 		}
248 | 
249 | 		return {
250 | 			isValid: errors.length === 0,
251 | 			errors,
252 | 			warnings
253 | 		};
254 | 	}
255 | 
256 | 	/**
257 | 	 * Reset configuration to defaults
258 | 	 */
259 | 	async resetToDefaults(): Promise<void> {
260 | 		const defaultConfig = this.getDefaultConfig();
261 | 		await this.updateConfig(defaultConfig);
262 | 	}
263 | 
264 | 	/**
265 | 	 * Export configuration to JSON
266 | 	 */
267 | 	exportConfig(): string {
268 | 		return JSON.stringify(this.config, null, 2);
269 | 	}
270 | 
271 | 	/**
272 | 	 * Import configuration from JSON
273 | 	 */
274 | 	async importConfig(jsonConfig: string): Promise<void> {
275 | 		try {
276 | 			const importedConfig = JSON.parse(jsonConfig) as TaskMasterConfig;
277 | 			const validation = this.validateConfig(importedConfig);
278 | 
279 | 			if (!validation.isValid) {
280 | 				throw new Error(
281 | 					`Invalid configuration: ${validation.errors.join(', ')}`
282 | 				);
283 | 			}
284 | 
285 | 			if (validation.warnings.length > 0) {
286 | 				const proceed = await vscode.window.showWarningMessage(
287 | 					`Configuration has warnings: ${validation.warnings.join(', ')}. Import anyway?`,
288 | 					'Yes',
289 | 					'No'
290 | 				);
291 | 
292 | 				if (proceed !== 'Yes') {
293 | 					return;
294 | 				}
295 | 			}
296 | 
297 | 			await this.updateConfig(importedConfig);
298 | 			vscode.window.showInformationMessage(
299 | 				'Configuration imported successfully'
300 | 			);
301 | 		} catch (error) {
302 | 			const errorMessage =
303 | 				error instanceof Error ? error.message : 'Unknown error';
304 | 			vscode.window.showErrorMessage(
305 | 				`Failed to import configuration: ${errorMessage}`
306 | 			);
307 | 			throw error;
308 | 		}
309 | 	}
310 | 
311 | 	/**
312 | 	 * Add configuration change listener
313 | 	 */
314 | 	onConfigChange(listener: (config: TaskMasterConfig) => void): void {
315 | 		this.configListeners.push(listener);
316 | 	}
317 | 
318 | 	/**
319 | 	 * Remove configuration change listener
320 | 	 */
321 | 	removeConfigListener(listener: (config: TaskMasterConfig) => void): void {
322 | 		const index = this.configListeners.indexOf(listener);
323 | 		if (index !== -1) {
324 | 			this.configListeners.splice(index, 1);
325 | 		}
326 | 	}
327 | 
328 | 	/**
329 | 	 * Load configuration from VS Code settings
330 | 	 */
331 | 	private loadConfig(): TaskMasterConfig {
332 | 		const vsConfig = vscode.workspace.getConfiguration('taskmaster');
333 | 		const defaultConfig = this.getDefaultConfig();
334 | 
335 | 		return {
336 | 			mcp: {
337 | 				command: vsConfig.get('mcp.command', defaultConfig.mcp.command),
338 | 				args: vsConfig.get('mcp.args', defaultConfig.mcp.args),
339 | 				cwd: vsConfig.get('mcp.cwd', defaultConfig.mcp.cwd),
340 | 				env: vsConfig.get('mcp.env', defaultConfig.mcp.env),
341 | 				timeout: vsConfig.get('mcp.timeout', defaultConfig.mcp.timeout),
342 | 				maxReconnectAttempts: vsConfig.get(
343 | 					'mcp.maxReconnectAttempts',
344 | 					defaultConfig.mcp.maxReconnectAttempts
345 | 				),
346 | 				reconnectBackoffMs: vsConfig.get(
347 | 					'mcp.reconnectBackoffMs',
348 | 					defaultConfig.mcp.reconnectBackoffMs
349 | 				),
350 | 				maxBackoffMs: vsConfig.get(
351 | 					'mcp.maxBackoffMs',
352 | 					defaultConfig.mcp.maxBackoffMs
353 | 				),
354 | 				healthCheckIntervalMs: vsConfig.get(
355 | 					'mcp.healthCheckIntervalMs',
356 | 					defaultConfig.mcp.healthCheckIntervalMs
357 | 				)
358 | 			},
359 | 			ui: {
360 | 				autoRefresh: vsConfig.get(
361 | 					'ui.autoRefresh',
362 | 					defaultConfig.ui.autoRefresh
363 | 				),
364 | 				refreshIntervalMs: vsConfig.get(
365 | 					'ui.refreshIntervalMs',
366 | 					defaultConfig.ui.refreshIntervalMs
367 | 				),
368 | 				theme: vsConfig.get('ui.theme', defaultConfig.ui.theme),
369 | 				showCompletedTasks: vsConfig.get(
370 | 					'ui.showCompletedTasks',
371 | 					defaultConfig.ui.showCompletedTasks
372 | 				),
373 | 				taskDisplayLimit: vsConfig.get(
374 | 					'ui.taskDisplayLimit',
375 | 					defaultConfig.ui.taskDisplayLimit
376 | 				),
377 | 				showPriority: vsConfig.get(
378 | 					'ui.showPriority',
379 | 					defaultConfig.ui.showPriority
380 | 				),
381 | 				showTaskIds: vsConfig.get(
382 | 					'ui.showTaskIds',
383 | 					defaultConfig.ui.showTaskIds
384 | 				)
385 | 			},
386 | 			performance: {
387 | 				maxConcurrentRequests: vsConfig.get(
388 | 					'performance.maxConcurrentRequests',
389 | 					defaultConfig.performance.maxConcurrentRequests
390 | 				),
391 | 				requestTimeoutMs: vsConfig.get(
392 | 					'performance.requestTimeoutMs',
393 | 					defaultConfig.performance.requestTimeoutMs
394 | 				),
395 | 				cacheTasksMs: vsConfig.get(
396 | 					'performance.cacheTasksMs',
397 | 					defaultConfig.performance.cacheTasksMs
398 | 				),
399 | 				lazyLoadThreshold: vsConfig.get(
400 | 					'performance.lazyLoadThreshold',
401 | 					defaultConfig.performance.lazyLoadThreshold
402 | 				)
403 | 			},
404 | 			debug: {
405 | 				enableLogging: vsConfig.get(
406 | 					'debug.enableLogging',
407 | 					defaultConfig.debug.enableLogging
408 | 				),
409 | 				logLevel: vsConfig.get('debug.logLevel', defaultConfig.debug.logLevel),
410 | 				enableConnectionMetrics: vsConfig.get(
411 | 					'debug.enableConnectionMetrics',
412 | 					defaultConfig.debug.enableConnectionMetrics
413 | 				),
414 | 				saveEventLogs: vsConfig.get(
415 | 					'debug.saveEventLogs',
416 | 					defaultConfig.debug.saveEventLogs
417 | 				),
418 | 				maxEventLogSize: vsConfig.get(
419 | 					'debug.maxEventLogSize',
420 | 					defaultConfig.debug.maxEventLogSize
421 | 				)
422 | 			}
423 | 		};
424 | 	}
425 | 
426 | 	/**
427 | 	 * Get default configuration
428 | 	 */
429 | 	private getDefaultConfig(): TaskMasterConfig {
430 | 		return {
431 | 			mcp: {
432 | 				command: 'npx',
433 | 				args: ['task-master-ai'],
434 | 				cwd: vscode.workspace.rootPath || '',
435 | 				env: undefined,
436 | 				timeout: 30000,
437 | 				maxReconnectAttempts: 5,
438 | 				reconnectBackoffMs: 1000,
439 | 				maxBackoffMs: 30000,
440 | 				healthCheckIntervalMs: 15000
441 | 			},
442 | 			ui: {
443 | 				autoRefresh: true,
444 | 				refreshIntervalMs: 10000,
445 | 				theme: 'auto',
446 | 				showCompletedTasks: true,
447 | 				taskDisplayLimit: 100,
448 | 				showPriority: true,
449 | 				showTaskIds: true
450 | 			},
451 | 			performance: {
452 | 				maxConcurrentRequests: 5,
453 | 				requestTimeoutMs: 30000,
454 | 				cacheTasksMs: 5000,
455 | 				lazyLoadThreshold: 50
456 | 			},
457 | 			debug: {
458 | 				enableLogging: true,
459 | 				logLevel: 'info',
460 | 				enableConnectionMetrics: true,
461 | 				saveEventLogs: false,
462 | 				maxEventLogSize: 1000
463 | 			}
464 | 		};
465 | 	}
466 | 
467 | 	/**
468 | 	 * Setup configuration watcher
469 | 	 */
470 | 	private setupConfigWatcher(): void {
471 | 		vscode.workspace.onDidChangeConfiguration((event) => {
472 | 			if (event.affectsConfiguration('taskmaster')) {
473 | 				logger.log('Task Master configuration changed, reloading...');
474 | 				this.config = this.loadConfig();
475 | 				this.notifyConfigChange();
476 | 			}
477 | 		});
478 | 	}
479 | 
480 | 	/**
481 | 	 * Merge configurations
482 | 	 */
483 | 	private mergeConfig(
484 | 		baseConfig: TaskMasterConfig,
485 | 		updates: Partial<TaskMasterConfig>
486 | 	): TaskMasterConfig {
487 | 		return {
488 | 			mcp: { ...baseConfig.mcp, ...updates.mcp },
489 | 			ui: { ...baseConfig.ui, ...updates.ui },
490 | 			performance: { ...baseConfig.performance, ...updates.performance },
491 | 			debug: { ...baseConfig.debug, ...updates.debug }
492 | 		};
493 | 	}
494 | 
495 | 	/**
496 | 	 * Notify configuration change listeners
497 | 	 */
498 | 	private notifyConfigChange(): void {
499 | 		this.configListeners.forEach((listener) => {
500 | 			try {
501 | 				listener(this.config);
502 | 			} catch (error) {
503 | 				logger.error('Error in configuration change listener:', error);
504 | 			}
505 | 		});
506 | 	}
507 | }
508 | 
509 | /**
510 |  * Utility function to get configuration manager instance
511 |  */
512 | export function getConfigManager(): ConfigManager {
513 | 	return ConfigManager.getInstance();
514 | }
515 | 
```

--------------------------------------------------------------------------------
/.taskmaster/docs/research/2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | title: Research Session
  3 | query: "do we need to add new commands or can we just weaponize the gh cli tool"
  4 | date: 2025-08-01
  5 | time: 2:27:52 p.m.
  6 | timestamp: 2025-08-01T11:27:52.249Z
  7 | exchanges: 3
  8 | ---
  9 | 
 10 | # Research Session
 11 | 
 12 | ## Initial Query
 13 | 
 14 | **Question:** do we need to add new commands or can we just weaponize the gh cli tool
 15 | 
 16 | **Response:**
 17 | 
 18 | For the implementation of the `task-master git` command suite to automate git workflows (Task 97), it is possible to leverage the existing **GitHub CLI (`gh`) tool** rather than creating entirely new git commands from scratch. The GitHub CLI provides a rich set of commands that cover many GitHub-related operations such as branch management, pull request handling, workflow triggering, and release management, which can be scripted and integrated into your CLI tool[1][2][3][4].
 19 | 
 20 | ### Key Points Supporting Weaponizing GitHub CLI:
 21 | 
 22 | - **Comprehensive GitHub Operations**: The `gh` CLI supports creating branches, managing pull requests, releases, tags, and even triggering and monitoring GitHub Actions workflows. This aligns well with the requirements of automated branch management, commit generation, and PR description generation in Task 97[1][3].
 23 | 
 24 | - **Scripting and Automation Friendly**: GitHub CLI commands can be scripted and integrated into custom workflows or CLI tools. You can create aliases or wrap `gh` commands inside your own CLI commands to provide a seamless user experience while relying on the battle-tested GitHub CLI for underlying operations[1][3].
 25 | 
 26 | - **Authentication and Permissions**: `gh` uses environment variables like `GH_TOKEN` or `GITHUB_TOKEN` for authentication, which can be securely managed in CI/CD pipelines or local environments. This simplifies integration without needing to implement custom authentication logic[3][4].
 27 | 
 28 | - **Extensibility**: If some specific git workflow features are not directly supported by `gh`, you can extend it with custom scripts or combine it with native git commands. This hybrid approach reduces the need to reinvent core git operations while allowing customization[1].
 29 | 
 30 | ### Considerations for Your Project:
 31 | 
 32 | - Since Task 97 requires **automated branch management** (branch creation, validation, switching, cleanup), **intelligent commit generation**, and **PR description generation**, you can implement these by invoking `gh` commands programmatically within your `task-master git` commands.
 33 | 
 34 | - For example, branch creation and switching can be done with native git commands wrapped in your CLI, while PR creation and management can be handled via `gh pr create`, `gh pr merge`, etc.
 35 | 
 36 | - You may still need to implement some **custom logic** for commit type detection, branch name validation, and PR description generation based on your task metadata, but the underlying git and GitHub operations can be delegated to `gh`.
 37 | 
 38 | - This approach aligns with your existing CLI foundation (Task 2) and leverages a well-supported tool, reducing development effort and increasing reliability.
 39 | 
 40 | ### Summary
 41 | 
 42 | You **do not necessarily need to add entirely new git commands** for Task 97. Instead, you can **weaponize the GitHub CLI (`gh`) tool** by wrapping and scripting its commands within your `task-master git` command suite. This will allow you to automate git workflows efficiently, maintain consistency, and leverage GitHub’s API capabilities without reinventing the wheel[1][2][3][4].
 43 | 
 44 | If there are gaps in `gh` functionality for your specific workflow needs, you can supplement with native git commands or custom scripts, but the core git and GitHub operations should be handled by `gh` for best results.
 45 | 
 46 | ---
 47 | 
 48 | ## Follow-up 1
 49 | 
 50 | **Question:** how does this relate to git worktrees? i hear that they get super complicated when trying to implement them.
 51 | 
 52 | **Response:**
 53 | 
 54 | Git worktrees allow you to have multiple working directories attached to the same Git repository, each checked out to different branches simultaneously. This enables parallel development workflows without the overhead of cloning the repository multiple times or constantly switching branches in a single working directory[2][3].
 55 | 
 56 | However, **implementing or managing git worktrees can get complicated** for several reasons:
 57 | 
 58 | - **Complexity for users and tooling:** Managing multiple active worktrees requires careful tracking of which directory corresponds to which branch. This can be confusing and error-prone, especially for novices or when many worktrees are in use simultaneously. Mistakes like committing to the wrong branch or working in the wrong directory are common risks[1].
 59 | 
 60 | - **Maintenance overhead:** Worktrees accumulate over time and need to be pruned or cleaned up regularly (`git worktree prune`) to avoid clutter and potential confusion. This adds an extra maintenance step to the workflow[1].
 61 | 
 62 | - **Syncing and update challenges:** Each worktree is a separate checkout and can become outdated if not regularly updated from the main repository. This can lead to merge conflicts or redundant work if branches diverge significantly[1].
 63 | 
 64 | - **Tooling and integration limitations:** Not all Git tools, scripts, or GUIs fully support worktrees or are optimized for them. This can cause inconsistencies or require additional custom tooling to manage worktrees effectively[1].
 65 | 
 66 | - **Performance considerations:** Managing multiple worktrees simultaneously can have performance implications, especially in very large repositories, due to the overhead of maintaining multiple checkouts and metadata[1].
 67 | 
 68 | In the context of your project, which involves automating git workflows and possibly wrapping or extending git/GitHub CLI commands, these complexities imply:
 69 | 
 70 | - If you consider supporting git worktrees in your `task-master git` command suite, you will need to handle the complexity of managing multiple directories and branches carefully.
 71 | 
 72 | - You might need to implement additional logic or tooling to track and manage worktrees, avoid user confusion, and ensure synchronization between them.
 73 | 
 74 | - Since your project already plans to leverage the GitHub CLI (`gh`) and native git commands, you could integrate worktree commands (`git worktree add`, `git worktree remove`, `git worktree list`) but should be prepared for the added complexity in workflow management and user documentation.
 75 | 
 76 | - Custom wrappers or managers (like the one described in [3]) can help reduce friction by automating worktree creation, switching, and cleanup, which might be a useful approach to consider.
 77 | 
 78 | Overall, **git worktrees provide powerful parallel branch management but introduce complexity in implementation and user experience** that must be carefully managed through tooling, automation, and clear documentation[1][3][4].
 79 | 
 80 | ---
 81 | 
 82 | **Summary Table of Git Worktree Challenges and Considerations**
 83 | 
 84 | | Aspect                  | Description                                                                                   | Implication for Project                                  |
 85 | |-------------------------|-----------------------------------------------------------------------------------------------|----------------------------------------------------------|
 86 | | User Complexity         | Confusing multiple directories and branches                                                  | Need clear UI/UX and safeguards                          |
 87 | | Maintenance Overhead    | Requires pruning and cleanup of stale worktrees                                              | Automate cleanup or provide commands                     |
 88 | | Syncing Issues         | Worktrees can become outdated or diverge                                                     | Implement update checks and sync workflows               |
 89 | | Tooling Support        | Limited support in some Git tools and GUIs                                                  | May require custom tooling or wrappers                   |
 90 | | Performance            | Overhead in large repos with many worktrees                                                 | Monitor and optimize performance                          |
 91 | | Workflow Automation    | Manual worktree management is tedious                                                       | Build custom managers or wrappers to simplify usage     |
 92 | 
 93 | ---
 94 | 
 95 | Given your project’s focus on automating git workflows and leveraging existing CLI tools, **implementing git worktree support is feasible but will require careful design to handle these complexities effectively**. You may want to start with basic worktree commands and incrementally add automation and safeguards as needed.
 96 | 
 97 | ---
 98 | 
 99 | ## Follow-up 2
100 | 
101 | **Question:** since we already have a way to have separated task lists per branch, couldn't we do the same thing for worktrees? each taskmaster list (tag) would have its own worktree based on the name of the tag?
102 | 
103 | **Response:**
104 | 
105 | Yes, you can apply a similar approach used for separated task lists per branch to git worktrees by associating each taskmaster list (tag) with its own git worktree named after the tag. This means each tag would correspond to a dedicated worktree directory checked out to a branch or commit related to that tag, enabling isolated parallel work environments for each task list.
106 | 
107 | ### How This Maps to Git Worktrees
108 | 
109 | - **Git worktrees** allow multiple working directories linked to the same repository, each checked out to a different branch or commit[1][2]. This lets you work on multiple branches simultaneously without switching contexts in a single directory.
110 | 
111 | - When you create a worktree with `git worktree add <path> [<branch>]`, Git creates a new linked working tree at `<path>`, optionally checking out a specified branch or creating a new one[1][2]. Naming the worktree directory after the tag (taskmaster list) is straightforward and semantically clear.
112 | 
113 | - This setup mirrors your existing branch-based task list separation but extends it to physical directories, which can improve workflow isolation and reduce context switching.
114 | 
115 | ### Benefits for Your Project
116 | 
117 | - **Isolated environments per task list/tag:** Each tag’s worktree can have its own checked-out branch, allowing independent development, testing, or review without interference.
118 | 
119 | - **Parallel workflows:** Developers can switch between worktrees (directories) instead of switching branches in one directory, reducing errors and improving productivity.
120 | 
121 | - **Automation potential:** Your `task-master git` commands can automate creating, switching, and cleaning up these worktrees based on tags, similar to how you manage branches and task lists.
122 | 
123 | ### Challenges and Considerations
124 | 
125 | - **Complexity in management:** Git worktrees add complexity in tracking multiple directories and ensuring users operate in the correct one[1]. Your CLI should provide clear commands and feedback to avoid confusion.
126 | 
127 | - **Maintenance overhead:** Worktrees need pruning and cleanup (`git worktree prune` or `git worktree remove`) to avoid clutter[1]. Automating cleanup in your tool would be beneficial.
128 | 
129 | - **Syncing and updates:** Each worktree is a separate checkout and can become outdated. Your automation should include commands to update or rebase worktrees as needed.
130 | 
131 | - **Tooling support:** Not all Git tools or scripts fully support worktrees, so your CLI wrappers should handle edge cases and provide a consistent experience[1][3].
132 | 
133 | ### Practical Implementation Ideas
134 | 
135 | - When a new tag (taskmaster list) is created, run `git worktree add` to create a new worktree directory named after the tag.
136 | 
137 | - Store metadata linking tags to worktree paths in your taskmaster configuration to track and manage them.
138 | 
139 | - Provide commands like `task-master worktree list`, `task-master worktree add <tag>`, and `task-master worktree remove <tag>` to manage these worktrees.
140 | 
141 | - Integrate with GitHub CLI (`gh`) and native git commands to automate branch creation, PR handling, and syncing within each worktree.
142 | 
143 | - Consider inspiration from existing tools like [wt](https://github.com/taecontrol/wt) that automate worktree environment setup and teardown[3].
144 | 
145 | ### Summary
146 | 
147 | Using git worktrees per taskmaster tag is a feasible and potentially powerful extension of your current branch-based task list separation. It offers isolated, parallel working directories aligned with your tags, improving workflow clarity and reducing branch-switching overhead. However, it requires careful tooling and automation to manage complexity, maintenance, and synchronization effectively. Given your project’s focus on automating git workflows and leveraging CLI tools, implementing this feature incrementally with robust user feedback and cleanup commands would be a practical approach.
148 | 
149 | 
150 | ---
151 | 
152 | *Generated by Task Master Research Command*  
153 | *Timestamp: 2025-08-01T11:27:52.249Z*
154 | 
```

--------------------------------------------------------------------------------
/tests/e2e/run_fallback_verification.sh:
--------------------------------------------------------------------------------

```bash
  1 | #!/bin/bash
  2 | 
  3 | # --- Fallback Model Verification Script ---
  4 | # Purpose: Tests models marked as 'fallback' in supported-models.json
  5 | #          to see if they work with generateObjectService (via update-subtask).
  6 | # Usage:   1. Run from within a prepared E2E test run directory:
  7 | #             ./path/to/script.sh .
  8 | #          2. Run from project root (or anywhere) to use the latest run dir:
  9 | #             ./tests/e2e/run_fallback_verification.sh
 10 | #          3. Run from project root (or anywhere) targeting a specific run dir:
 11 | #             ./tests/e2e/run_fallback_verification.sh /path/to/tests/e2e/_runs/run_YYYYMMDD_HHMMSS
 12 | # Output: Prints a summary report to standard output. Errors to standard error.
 13 | 
 14 | # Treat unset variables as an error when substituting.
 15 | set -u
 16 | # Prevent errors in pipelines from being masked.
 17 | set -o pipefail
 18 | 
 19 | # --- Embedded Helper Functions ---
 20 | # Copied from e2e_helpers.sh to make this script standalone
 21 | # OR source it if preferred and path is reliable
 22 | 
 23 | # <<< Determine SCRIPT_DIR and PROJECT_ROOT_DIR early >>>
 24 | SCRIPT_DIR_FV="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
 25 | PROJECT_ROOT_DIR_FV="$( cd "$SCRIPT_DIR_FV/../.." &> /dev/null && pwd )" # Assumes script is in tests/e2e/
 26 | 
 27 | # --- Try to Source e2e_helpers.sh ---
 28 | E2E_HELPERS_PATH_FV="${PROJECT_ROOT_DIR_FV}/tests/e2e/e2e_helpers.sh"
 29 | if [ -f "$E2E_HELPERS_PATH_FV" ]; then
 30 |     # shellcheck source=tests/e2e/e2e_helpers.sh
 31 |     source "$E2E_HELPERS_PATH_FV"
 32 |     echo "[INFO FV] Sourced e2e_helpers.sh successfully."
 33 | else
 34 |     echo "[ERROR FV] e2e_helpers.sh not found at $E2E_HELPERS_PATH_FV. Cost extraction will fail."
 35 |     # Define a placeholder if not found, so the script doesn't break immediately,
 36 |     # but cost extraction will effectively be a no-op.
 37 |     extract_and_sum_cost() { echo "$2"; } # Returns current total, effectively adding 0
 38 | fi
 39 | 
 40 | 
 41 | _format_duration() {
 42 |   local total_seconds=$1
 43 |   local minutes=$((total_seconds / 60))
 44 |   local seconds=$((total_seconds % 60))
 45 |   printf "%dm%02ds" "$minutes" "$seconds"
 46 | }
 47 | 
 48 | _get_elapsed_time_for_log() {
 49 |   local current_time
 50 |   current_time=$(date +%s)
 51 |   local elapsed_seconds
 52 |   elapsed_seconds=$((current_time - overall_start_time)) # Needs overall_start_time
 53 |   _format_duration "$elapsed_seconds"
 54 | }
 55 | 
 56 | log_info() {
 57 |   echo "[INFO FV] [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
 58 | }
 59 | 
 60 | log_success() {
 61 |   echo "[SUCCESS FV] [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
 62 | }
 63 | 
 64 | log_error() {
 65 |   echo "[ERROR FV] [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1" >&2
 66 | }
 67 | 
 68 | log_step() {
 69 |   test_step_count=$((test_step_count + 1)) # Needs test_step_count
 70 |   echo ""
 71 |   echo "============================================="
 72 |   echo "  FV STEP ${test_step_count}: [$(_get_elapsed_time_for_log)] $(date +"%Y-%m-%d %H:%M:%S") $1"
 73 |   echo "============================================="
 74 | }
 75 | 
 76 | # --- Signal Handling ---
 77 | child_pid=0
 78 | PROGRESS_LOG_FILE="fallback_verification_progress.log" # Stays in run dir
 79 | 
 80 | cleanup() {
 81 |     echo ""
 82 |     log_error "Interrupt received. Cleaning up any running child process..."
 83 |     if [ "$child_pid" -ne 0 ]; then
 84 |         log_info "Killing child process (PID: $child_pid) and its group..."
 85 |         kill -TERM -- "-$child_pid" 2>/dev/null || kill -KILL -- "-$child_pid" 2>/dev/null
 86 |         child_pid=0
 87 |     fi
 88 |     log_info "Progress saved in: $PROGRESS_LOG_FILE"
 89 |     # Print current total cost on interrupt
 90 |     if [[ -n "${total_fallback_cost+x}" && "$total_fallback_cost" != "0.0" ]]; then # Check if var is set and not initial
 91 |         log_info "Current Total Fallback AI Cost at interruption: $total_fallback_cost USD"
 92 |     fi
 93 |     exit 130
 94 | }
 95 | 
 96 | trap cleanup INT TERM
 97 | 
 98 | # --- Configuration ---
 99 | # SCRIPT_DIR and PROJECT_ROOT_DIR already defined above
100 | SUPPORTED_MODELS_FILE="$PROJECT_ROOT_DIR_FV/scripts/modules/supported-models.json"
101 | BASE_RUNS_DIR="$PROJECT_ROOT_DIR_FV/tests/e2e/_runs"
102 | 
103 | # --- Determine Target Run Directory ---
104 | TARGET_RUN_DIR=""
105 | if [ "$#" -ge 1 ] && [ -n "$1" ]; then
106 |     TARGET_RUN_DIR="$1"
107 |     if [[ "$TARGET_RUN_DIR" != /* ]]; then
108 |         TARGET_RUN_DIR="$(pwd)/$TARGET_RUN_DIR"
109 |     fi
110 |     echo "[INFO FV] Using provided target run directory: $TARGET_RUN_DIR"
111 | else
112 |     echo "[INFO FV] No run directory provided, finding latest in $BASE_RUNS_DIR..."
113 |     TARGET_RUN_DIR=$(ls -td "$BASE_RUNS_DIR"/run_* 2>/dev/null | head -n 1)
114 |     if [ -z "$TARGET_RUN_DIR" ]; then
115 |         echo "[ERROR FV] No run directories found matching 'run_*' in $BASE_RUNS_DIR. Cannot proceed." >&2
116 |         exit 1
117 |     fi
118 |      echo "[INFO FV] Found latest run directory: $TARGET_RUN_DIR"
119 | fi
120 | 
121 | if [ ! -d "$TARGET_RUN_DIR" ]; then
122 |     echo "[ERROR FV] Target run directory not found or is not a directory: $TARGET_RUN_DIR" >&2
123 |     exit 1
124 | fi
125 | 
126 | echo "[INFO FV] Changing working directory to: $TARGET_RUN_DIR"
127 | if ! cd "$TARGET_RUN_DIR"; then
128 |      echo "[ERROR FV] Failed to cd into target directory: $TARGET_RUN_DIR" >&2
129 |      exit 1
130 | fi
131 | echo "[INFO FV] Now operating inside: $(pwd)"
132 | 
133 | overall_start_time=$(date +%s) # Initialize for logging helpers
134 | test_step_count=0               # Initialize for logging helpers
135 | total_fallback_cost="0.0"       # Initialize total cost for this script
136 | 
137 | log_info "Starting fallback verification script execution in $(pwd)"
138 | log_info "Progress will be logged to: $(pwd)/$PROGRESS_LOG_FILE"
139 | 
140 | log_step "Checking for dependencies (jq, bc) in verification script"
141 | if ! command -v jq &> /dev/null; then log_error "Dependency 'jq' not installed."; exit 1; fi
142 | if ! command -v bc &> /dev/null; then log_error "Dependency 'bc' not installed (for cost calculation)."; exit 1; fi
143 | log_success "Dependencies 'jq' and 'bc' found."
144 | 
145 | 
146 | log_step "Starting/Resuming Fallback Model (generateObjectService) Verification"
147 | touch "$PROGRESS_LOG_FILE"
148 | 
149 | if [ ! -f "$SUPPORTED_MODELS_FILE" ]; then
150 |     log_error "supported-models.json not found at: $SUPPORTED_MODELS_FILE."
151 |     exit 1
152 | fi
153 | log_info "Using supported models file: $SUPPORTED_MODELS_FILE"
154 | 
155 | if [ ! -f "tasks/tasks.json" ]; then
156 |     log_error "tasks/tasks.json not found in current directory ($(pwd)). Was this run directory properly initialized?"
157 |     exit 1
158 | fi
159 | if ! jq -e '.tasks[] | select(.id == 1) | .subtasks[] | select(.id == 1)' tasks/tasks.json > /dev/null 2>&1; then
160 |     log_error "Subtask 1.1 not found in tasks.json within $(pwd). Cannot perform update-subtask tests."
161 |     exit 1
162 | fi
163 | log_info "Subtask 1.1 found in $(pwd)/tasks/tasks.json, proceeding with verification."
164 | 
165 | jq -c 'to_entries[] | .key as $provider | .value[] | select(.allowed_roles[]? == "fallback") | {provider: $provider, id: .id}' "$SUPPORTED_MODELS_FILE" | while IFS= read -r model_info; do
166 |     provider=$(echo "$model_info" | jq -r '.provider')
167 |     model_id=$(echo "$model_info" | jq -r '.id')
168 |     flag=""
169 | 
170 |     if grep -Fq "${provider},${model_id}," "$PROGRESS_LOG_FILE"; then
171 |         log_info "--- Skipping: $provider / $model_id (already tested, result in $PROGRESS_LOG_FILE) ---"
172 |         # Still need to sum up its cost if it was successful before
173 |         previous_test_output=$(grep -F "${provider},${model_id}," "$PROGRESS_LOG_FILE" | head -n 1)
174 |         # Assuming the output file for successful test exists and contains cost
175 |         prev_output_file="update_subtask_raw_output_${provider}_${model_id//\//_}.log"
176 |         if [[ "$previous_test_output" == *",SUCCESS"* && -f "$prev_output_file" ]]; then
177 |             # shellcheck disable=SC2154 # overall_start_time is set
178 |             log_info "Summing cost from previous successful test of $provider / $model_id from $prev_output_file"
179 |             # shellcheck disable=SC2154 # total_fallback_cost is set
180 |             total_fallback_cost=$(extract_and_sum_cost "$(cat "$prev_output_file")" "$total_fallback_cost")
181 |             log_info "Cumulative fallback AI cost after previous $provider / $model_id: $total_fallback_cost USD"
182 |         fi
183 |         continue
184 |     fi
185 | 
186 |     log_info "--- Verifying: $provider / $model_id ---"
187 | 
188 |     if [ "$provider" == "openrouter" ]; then flag="--openrouter"; fi
189 |     if [ "$provider" == "ollama" ]; then flag="--ollama"; fi
190 | 
191 |     if ! command -v task-master &> /dev/null; then
192 |         log_error "task-master command not found."
193 |         echo "[INSTRUCTION FV] Please run 'npm link task-master-ai' in the project root first."
194 |         exit 1
195 |     fi
196 |     log_info "Setting main model to $model_id ${flag:+using flag $flag}..."
197 |     set_model_cmd="task-master models --set-main \"$model_id\" $flag"
198 |     if ! eval "$set_model_cmd" > /dev/null 2>&1; then
199 |         log_error "Failed to set main model for $provider / $model_id. Skipping test."
200 |         echo "$provider,$model_id,SET_MODEL_FAILED" >> "$PROGRESS_LOG_FILE"
201 |         continue
202 |     fi
203 |     log_info "Set main model ok."
204 | 
205 |     log_info "Running update-subtask --id=1.1 --prompt='Test generateObjectService' (timeout 120s)"
206 |     update_subtask_output_file="update_subtask_raw_output_${provider}_${model_id//\//_}.log"
207 |     
208 |     # Capture output to a variable AND a file
209 |     update_subtask_command_output=""
210 |     timeout 120s task-master update-subtask --id=1.1 --prompt="Simple test prompt to verify generateObjectService call." 2>&1 | tee "$update_subtask_output_file" &
211 |     # Store the command output in a variable simultaneously
212 |     # update_subtask_command_output=$(timeout 120s task-master update-subtask --id=1.1 --prompt="Simple test prompt to verify generateObjectService call." 2>&1)
213 |     # The above direct capture won't work well with tee and backgrounding. Instead, read the file after command completion.
214 |     child_pid=$!
215 |     wait "$child_pid"
216 |     update_subtask_exit_code=$?
217 |     child_pid=0
218 | 
219 |     # Read output from file for cost extraction
220 |     if [ -f "$update_subtask_output_file" ]; then
221 |         update_subtask_command_output=$(cat "$update_subtask_output_file")
222 |     else
223 |         update_subtask_command_output="" # Ensure it's defined
224 |     fi
225 | 
226 |     result_status=""
227 |     if [ $update_subtask_exit_code -eq 0 ] && echo "$update_subtask_command_output" | grep -q "Successfully updated subtask #1.1"; then
228 |         log_success "update-subtask succeeded for $provider / $model_id (Verified Output)."
229 |         result_status="SUCCESS"
230 |         # Extract and sum cost if successful
231 |         # shellcheck disable=SC2154 # total_fallback_cost is set
232 |         total_fallback_cost=$(extract_and_sum_cost "$update_subtask_command_output" "$total_fallback_cost")
233 |         log_info "Cumulative fallback AI cost after $provider / $model_id: $total_fallback_cost USD"
234 |     elif [ $update_subtask_exit_code -eq 124 ]; then
235 |         log_error "update-subtask TIMED OUT for $provider / $model_id. Check $update_subtask_output_file."
236 |         result_status="FAILED_TIMEOUT"
237 |     elif [ $update_subtask_exit_code -eq 130 ] || [ $update_subtask_exit_code -eq 143 ]; then
238 |          log_error "update-subtask INTERRUPTED for $provider / $model_id."
239 |          result_status="INTERRUPTED"
240 |     else
241 |         log_error "update-subtask FAILED for $provider / $model_id (Exit Code: $update_subtask_exit_code). Check $update_subtask_output_file."
242 |         result_status="FAILED"
243 |     fi
244 | 
245 |     echo "$provider,$model_id,$result_status" >> "$PROGRESS_LOG_FILE"
246 | 
247 | done
248 | 
249 | echo ""
250 | echo "--- Fallback Model Verification Report (via $0) ---"
251 | echo "Executed inside run directory: $(pwd)"
252 | echo "Progress log: $(pwd)/$PROGRESS_LOG_FILE"
253 | echo ""
254 | echo "Test Command: task-master update-subtask --id=1.1 --prompt=\"...\" (tests generateObjectService)"
255 | echo "Models were tested by setting them as the 'main' model temporarily."
256 | echo "Results based on exit code and output verification:"
257 | echo ""
258 | echo "Models CONFIRMED to support generateObjectService (Keep 'fallback' role):"
259 | awk -F',' '$3 == "SUCCESS" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
260 | echo ""
261 | echo "Models FAILED generateObjectService test (Suggest REMOVING 'fallback' role):"
262 | awk -F',' '$3 == "FAILED" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
263 | echo ""
264 | echo "Models TIMED OUT during test (Suggest REMOVING 'fallback' role):"
265 | awk -F',' '$3 == "FAILED_TIMEOUT" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
266 | echo ""
267 | echo "Models where setting the model failed (Inconclusive):"
268 | awk -F',' '$3 == "SET_MODEL_FAILED" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
269 | echo ""
270 | echo "Models INTERRUPTED during test (Inconclusive - Rerun):"
271 | awk -F',' '$3 == "INTERRUPTED" { print "- " $1 " / " $2 }' "$PROGRESS_LOG_FILE" | sort
272 | echo ""
273 | # Print the total cost for this script's operations
274 | formatted_total_fallback_cost=$(printf "%.6f" "$total_fallback_cost")
275 | echo "Total Fallback AI Cost (this script run): $formatted_total_fallback_cost USD" # This line will be parsed
276 | echo "-------------------------------------------------------"
277 | echo ""
278 | 
279 | log_info "Finished Fallback Model (generateObjectService) Verification Script"
280 | 
281 | trap - INT TERM
282 | exit 0
283 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/common/interfaces/storage.interface.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Storage interface definitions for the tm-core package
  3 |  * This file defines the contract for all storage implementations
  4 |  */
  5 | 
  6 | import type { ExpandTaskResult } from '../../modules/integration/services/task-expansion.service.js';
  7 | import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
  8 | 
  9 | /**
 10 |  * Options for loading tasks from storage
 11 |  */
 12 | export interface LoadTasksOptions {
 13 | 	/** Filter tasks by status */
 14 | 	status?: TaskStatus;
 15 | 	/** Exclude subtasks from loaded tasks (default: false) */
 16 | 	excludeSubtasks?: boolean;
 17 | }
 18 | 
 19 | /**
 20 |  * Result type for updateTaskStatus operations
 21 |  */
 22 | export interface UpdateStatusResult {
 23 | 	success: boolean;
 24 | 	oldStatus: TaskStatus;
 25 | 	newStatus: TaskStatus;
 26 | 	taskId: string;
 27 | }
 28 | 
 29 | /**
 30 |  * Interface for storage operations on tasks
 31 |  * All storage implementations must implement this interface
 32 |  */
 33 | export interface IStorage {
 34 | 	/**
 35 | 	 * Load all tasks from storage, optionally filtered by tag and other criteria
 36 | 	 * @param tag - Optional tag to filter tasks by
 37 | 	 * @param options - Optional filtering options (status, excludeSubtasks)
 38 | 	 * @returns Promise that resolves to an array of tasks
 39 | 	 */
 40 | 	loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]>;
 41 | 
 42 | 	/**
 43 | 	 * Load a single task by ID
 44 | 	 * @param taskId - ID of the task to load
 45 | 	 * @param tag - Optional tag context for the task
 46 | 	 * @returns Promise that resolves to the task or null if not found
 47 | 	 */
 48 | 	loadTask(taskId: string, tag?: string): Promise<Task | null>;
 49 | 
 50 | 	/**
 51 | 	 * Save tasks to storage, replacing existing tasks
 52 | 	 * @param tasks - Array of tasks to save
 53 | 	 * @param tag - Optional tag context for the tasks
 54 | 	 * @returns Promise that resolves when save is complete
 55 | 	 */
 56 | 	saveTasks(tasks: Task[], tag?: string): Promise<void>;
 57 | 
 58 | 	/**
 59 | 	 * Append new tasks to existing storage without replacing
 60 | 	 * @param tasks - Array of tasks to append
 61 | 	 * @param tag - Optional tag context for the tasks
 62 | 	 * @returns Promise that resolves when append is complete
 63 | 	 */
 64 | 	appendTasks(tasks: Task[], tag?: string): Promise<void>;
 65 | 
 66 | 	/**
 67 | 	 * Update a specific task by ID (direct structural update)
 68 | 	 * @param taskId - ID of the task to update
 69 | 	 * @param updates - Partial task object with fields to update
 70 | 	 * @param tag - Optional tag context for the task
 71 | 	 * @returns Promise that resolves when update is complete
 72 | 	 */
 73 | 	updateTask(
 74 | 		taskId: string,
 75 | 		updates: Partial<Task>,
 76 | 		tag?: string
 77 | 	): Promise<void>;
 78 | 
 79 | 	/**
 80 | 	 * Update a task using AI-powered prompt (natural language update)
 81 | 	 * @param taskId - ID of the task to update
 82 | 	 * @param prompt - Natural language prompt describing the update
 83 | 	 * @param tag - Optional tag context for the task
 84 | 	 * @param options - Optional update options
 85 | 	 * @param options.useResearch - Whether to use research capabilities (for file storage)
 86 | 	 * @param options.mode - Update mode: 'append' adds info, 'update' makes targeted changes, 'rewrite' restructures (for API storage)
 87 | 	 * @returns Promise that resolves when update is complete
 88 | 	 */
 89 | 	updateTaskWithPrompt(
 90 | 		taskId: string,
 91 | 		prompt: string,
 92 | 		tag?: string,
 93 | 		options?: { useResearch?: boolean; mode?: 'append' | 'update' | 'rewrite' }
 94 | 	): Promise<void>;
 95 | 
 96 | 	/**
 97 | 	 * Expand task into subtasks using AI-powered generation
 98 | 	 * @param taskId - ID of the task to expand
 99 | 	 * @param tag - Optional tag context for the task
100 | 	 * @param options - Optional expansion options
101 | 	 * @param options.numSubtasks - Number of subtasks to generate
102 | 	 * @param options.useResearch - Whether to use research capabilities
103 | 	 * @param options.additionalContext - Additional context for generation
104 | 	 * @param options.force - Force regeneration even if subtasks exist
105 | 	 * @returns ExpandTaskResult for API storage, void for file storage
106 | 	 */
107 | 	expandTaskWithPrompt(
108 | 		taskId: string,
109 | 		tag?: string,
110 | 		options?: {
111 | 			numSubtasks?: number;
112 | 			useResearch?: boolean;
113 | 			additionalContext?: string;
114 | 			force?: boolean;
115 | 		}
116 | 	): Promise<ExpandTaskResult | void>;
117 | 
118 | 	/**
119 | 	 * Update task or subtask status by ID
120 | 	 * @param taskId - ID of the task or subtask (e.g., "1" or "1.2")
121 | 	 * @param newStatus - New status to set
122 | 	 * @param tag - Optional tag context for the task
123 | 	 * @returns Promise that resolves to update result with old and new status
124 | 	 */
125 | 	updateTaskStatus(
126 | 		taskId: string,
127 | 		newStatus: TaskStatus,
128 | 		tag?: string
129 | 	): Promise<UpdateStatusResult>;
130 | 
131 | 	/**
132 | 	 * Delete a task by ID
133 | 	 * @param taskId - ID of the task to delete
134 | 	 * @param tag - Optional tag context for the task
135 | 	 * @returns Promise that resolves when deletion is complete
136 | 	 */
137 | 	deleteTask(taskId: string, tag?: string): Promise<void>;
138 | 
139 | 	/**
140 | 	 * Check if tasks exist in storage for the given tag
141 | 	 * @param tag - Optional tag to check existence for
142 | 	 * @returns Promise that resolves to boolean indicating existence
143 | 	 */
144 | 	exists(tag?: string): Promise<boolean>;
145 | 
146 | 	/**
147 | 	 * Load metadata about the task collection
148 | 	 * @param tag - Optional tag to get metadata for
149 | 	 * @returns Promise that resolves to task metadata
150 | 	 */
151 | 	loadMetadata(tag?: string): Promise<TaskMetadata | null>;
152 | 
153 | 	/**
154 | 	 * Save metadata about the task collection
155 | 	 * @param metadata - Metadata object to save
156 | 	 * @param tag - Optional tag context for the metadata
157 | 	 * @returns Promise that resolves when save is complete
158 | 	 */
159 | 	saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void>;
160 | 
161 | 	/**
162 | 	 * Get all available tags in storage
163 | 	 * @returns Promise that resolves to array of available tags
164 | 	 */
165 | 	getAllTags(): Promise<string[]>;
166 | 
167 | 	/**
168 | 	 * Create a new tag
169 | 	 * @param tagName - Name of the tag to create
170 | 	 * @param options - Creation options
171 | 	 * @param options.copyFrom - Tag to copy tasks from (optional)
172 | 	 * @param options.description - Tag description (optional)
173 | 	 * @returns Promise that resolves when creation is complete
174 | 	 */
175 | 	createTag(
176 | 		tagName: string,
177 | 		options?: { copyFrom?: string; description?: string }
178 | 	): Promise<void>;
179 | 
180 | 	/**
181 | 	 * Delete all tasks and metadata for a specific tag
182 | 	 * @param tag - Tag to delete
183 | 	 * @returns Promise that resolves when deletion is complete
184 | 	 */
185 | 	deleteTag(tag: string): Promise<void>;
186 | 
187 | 	/**
188 | 	 * Rename a tag (move all tasks from old tag to new tag)
189 | 	 * @param oldTag - Current tag name
190 | 	 * @param newTag - New tag name
191 | 	 * @returns Promise that resolves when rename is complete
192 | 	 */
193 | 	renameTag(oldTag: string, newTag: string): Promise<void>;
194 | 
195 | 	/**
196 | 	 * Copy all tasks from one tag to another
197 | 	 * @param sourceTag - Source tag to copy from
198 | 	 * @param targetTag - Target tag to copy to
199 | 	 * @returns Promise that resolves when copy is complete
200 | 	 */
201 | 	copyTag(sourceTag: string, targetTag: string): Promise<void>;
202 | 
203 | 	/**
204 | 	 * Initialize storage (create necessary directories, files, etc.)
205 | 	 * @returns Promise that resolves when initialization is complete
206 | 	 */
207 | 	initialize(): Promise<void>;
208 | 
209 | 	/**
210 | 	 * Clean up and close storage connections
211 | 	 * @returns Promise that resolves when cleanup is complete
212 | 	 */
213 | 	close(): Promise<void>;
214 | 
215 | 	/**
216 | 	 * Get storage statistics (file sizes, task counts, etc.)
217 | 	 * @returns Promise that resolves to storage statistics
218 | 	 */
219 | 	getStats(): Promise<StorageStats>;
220 | 
221 | 	/**
222 | 	 * Get the storage type identifier
223 | 	 * @returns The type of storage implementation ('file' or 'api')
224 | 	 */
225 | 	getStorageType(): 'file' | 'api';
226 | 
227 | 	/**
228 | 	 * Get the current brief name (only applicable for API storage)
229 | 	 * @returns The brief name if using API storage with a selected brief, null otherwise
230 | 	 */
231 | 	getCurrentBriefName(): string | null;
232 | 
233 | 	/**
234 | 	 * Get all tags with detailed statistics including task counts
235 | 	 * @returns Promise that resolves to tags with statistics
236 | 	 */
237 | 	getTagsWithStats(): Promise<TagsWithStatsResult>;
238 | }
239 | 
240 | /**
241 |  * Tag information with detailed statistics
242 |  */
243 | export interface TagInfo {
244 | 	/** Tag/Brief name */
245 | 	name: string;
246 | 	/** Whether this is the current/active tag */
247 | 	isCurrent: boolean;
248 | 	/** Total number of tasks in this tag */
249 | 	taskCount: number;
250 | 	/** Number of completed tasks */
251 | 	completedTasks: number;
252 | 	/** Breakdown of tasks by status */
253 | 	statusBreakdown: Record<string, number>;
254 | 	/** Subtask counts if available */
255 | 	subtaskCounts?: {
256 | 		totalSubtasks: number;
257 | 		subtasksByStatus: Record<string, number>;
258 | 	};
259 | 	/** Tag creation date */
260 | 	created?: string;
261 | 
262 | 	/** Tag last modified date */
263 | 	updatedAt?: string;
264 | 
265 | 	/** Tag description */
266 | 	description?: string;
267 | 	/** Brief/Tag status (for API storage briefs) */
268 | 	status?: string;
269 | 	/** Brief ID/UUID (for API storage) */
270 | 	briefId?: string;
271 | }
272 | 
273 | /**
274 |  * Result returned by getTagsWithStats
275 |  */
276 | export interface TagsWithStatsResult {
277 | 	/** List of tags with statistics */
278 | 	tags: TagInfo[];
279 | 	/** Current active tag name */
280 | 	currentTag: string | null;
281 | 	/** Total number of tags */
282 | 	totalTags: number;
283 | }
284 | 
285 | /**
286 |  * Storage statistics interface
287 |  */
288 | export interface StorageStats {
289 | 	/** Total number of tasks across all tags */
290 | 	totalTasks: number;
291 | 	/** Total number of tags */
292 | 	totalTags: number;
293 | 	/** Storage size in bytes */
294 | 	storageSize: number;
295 | 	/** Last modified timestamp */
296 | 	lastModified: string;
297 | 	/** Available tags with task counts */
298 | 	tagStats: Array<{
299 | 		tag: string;
300 | 		taskCount: number;
301 | 		lastModified: string;
302 | 	}>;
303 | }
304 | 
305 | /**
306 |  * Configuration options for storage implementations
307 |  */
308 | export interface StorageConfig {
309 | 	/** Base path for storage */
310 | 	basePath: string;
311 | 	/** Enable backup creation */
312 | 	enableBackup?: boolean;
313 | 	/** Maximum number of backups to keep */
314 | 	maxBackups?: number;
315 | 	/** Enable compression for storage */
316 | 	enableCompression?: boolean;
317 | 	/** File encoding (default: utf8) */
318 | 	encoding?: BufferEncoding;
319 | 	/** Enable atomic writes */
320 | 	atomicWrites?: boolean;
321 | }
322 | 
323 | /**
324 |  * Base abstract class for storage implementations
325 |  * Provides common functionality and enforces the interface
326 |  */
327 | export abstract class BaseStorage implements IStorage {
328 | 	protected config: StorageConfig;
329 | 
330 | 	constructor(config: StorageConfig) {
331 | 		this.config = config;
332 | 	}
333 | 
334 | 	// Abstract methods that must be implemented by concrete classes
335 | 	abstract loadTasks(tag?: string, options?: LoadTasksOptions): Promise<Task[]>;
336 | 	abstract loadTask(taskId: string, tag?: string): Promise<Task | null>;
337 | 	abstract saveTasks(tasks: Task[], tag?: string): Promise<void>;
338 | 	abstract appendTasks(tasks: Task[], tag?: string): Promise<void>;
339 | 	abstract updateTask(
340 | 		taskId: string,
341 | 		updates: Partial<Task>,
342 | 		tag?: string
343 | 	): Promise<void>;
344 | 	abstract updateTaskWithPrompt(
345 | 		taskId: string,
346 | 		prompt: string,
347 | 		tag?: string,
348 | 		options?: { useResearch?: boolean; mode?: 'append' | 'update' | 'rewrite' }
349 | 	): Promise<void>;
350 | 	abstract expandTaskWithPrompt(
351 | 		taskId: string,
352 | 		tag?: string,
353 | 		options?: {
354 | 			numSubtasks?: number;
355 | 			useResearch?: boolean;
356 | 			additionalContext?: string;
357 | 			force?: boolean;
358 | 		}
359 | 	): Promise<ExpandTaskResult | void>;
360 | 	abstract updateTaskStatus(
361 | 		taskId: string,
362 | 		newStatus: TaskStatus,
363 | 		tag?: string
364 | 	): Promise<UpdateStatusResult>;
365 | 	abstract deleteTask(taskId: string, tag?: string): Promise<void>;
366 | 	abstract exists(tag?: string): Promise<boolean>;
367 | 	abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;
368 | 	abstract saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void>;
369 | 	abstract getAllTags(): Promise<string[]>;
370 | 	abstract createTag(
371 | 		tagName: string,
372 | 		options?: { copyFrom?: string; description?: string }
373 | 	): Promise<void>;
374 | 	abstract deleteTag(tag: string): Promise<void>;
375 | 	abstract renameTag(oldTag: string, newTag: string): Promise<void>;
376 | 	abstract copyTag(sourceTag: string, targetTag: string): Promise<void>;
377 | 	abstract initialize(): Promise<void>;
378 | 	abstract close(): Promise<void>;
379 | 	abstract getStats(): Promise<StorageStats>;
380 | 	abstract getStorageType(): 'file' | 'api';
381 | 	abstract getCurrentBriefName(): string | null;
382 | 	abstract getTagsWithStats(): Promise<TagsWithStatsResult>;
383 | 	/**
384 | 	 * Utility method to generate backup filename
385 | 	 * @param originalPath - Original file path
386 | 	 * @returns Backup file path with timestamp
387 | 	 */
388 | 	protected generateBackupPath(originalPath: string): string {
389 | 		const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
390 | 		const parts = originalPath.split('.');
391 | 		const extension = parts.pop();
392 | 		const baseName = parts.join('.');
393 | 		return `${baseName}.backup.${timestamp}.${extension}`;
394 | 	}
395 | 
396 | 	/**
397 | 	 * Utility method to validate task data before storage operations
398 | 	 * @param task - Task to validate
399 | 	 * @throws Error if task is invalid
400 | 	 */
401 | 	protected validateTask(task: Task): void {
402 | 		if (!task.id) {
403 | 			throw new Error('Task ID is required');
404 | 		}
405 | 		if (!task.title) {
406 | 			throw new Error('Task title is required');
407 | 		}
408 | 		if (!task.description) {
409 | 			throw new Error('Task description is required');
410 | 		}
411 | 		if (!task.status) {
412 | 			throw new Error('Task status is required');
413 | 		}
414 | 	}
415 | 
416 | 	/**
417 | 	 * Utility method to sanitize tag names for file system safety
418 | 	 * @param tag - Tag name to sanitize
419 | 	 * @returns Sanitized tag name
420 | 	 */
421 | 	protected sanitizeTag(tag: string): string {
422 | 		return tag.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
423 | 	}
424 | }
425 | 
```

--------------------------------------------------------------------------------
/tests/unit/parse-prd.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | // In tests/unit/parse-prd.test.js
  2 | // Testing parse-prd.js file extension compatibility with real files
  3 | 
  4 | import { jest } from '@jest/globals';
  5 | import fs from 'fs';
  6 | import path from 'path';
  7 | import { fileURLToPath } from 'url';
  8 | import os from 'os';
  9 | 
 10 | const __filename = fileURLToPath(import.meta.url);
 11 | const __dirname = path.dirname(__filename);
 12 | 
 13 | // Mock the AI services to avoid real API calls
 14 | jest.unstable_mockModule(
 15 | 	'../../scripts/modules/ai-services-unified.js',
 16 | 	() => ({
 17 | 		streamTextService: jest.fn(),
 18 | 		generateObjectService: jest.fn(),
 19 | 		streamObjectService: jest.fn().mockImplementation(async () => {
 20 | 			return {
 21 | 				get partialObjectStream() {
 22 | 					return (async function* () {
 23 | 						yield { tasks: [] };
 24 | 						yield { tasks: [{ id: 1, title: 'Test Task', priority: 'high' }] };
 25 | 					})();
 26 | 				},
 27 | 				object: Promise.resolve({
 28 | 					tasks: [{ id: 1, title: 'Test Task', priority: 'high' }]
 29 | 				})
 30 | 			};
 31 | 		})
 32 | 	})
 33 | );
 34 | 
 35 | // Mock all config-manager exports comprehensively
 36 | jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
 37 | 	getDebugFlag: jest.fn(() => false),
 38 | 	getDefaultPriority: jest.fn(() => 'medium'),
 39 | 	getMainModelId: jest.fn(() => 'test-model'),
 40 | 	getResearchModelId: jest.fn(() => 'test-research-model'),
 41 | 	getParametersForRole: jest.fn(() => ({ maxTokens: 1000, temperature: 0.7 })),
 42 | 	getMainProvider: jest.fn(() => 'anthropic'),
 43 | 	getResearchProvider: jest.fn(() => 'perplexity'),
 44 | 	getFallbackProvider: jest.fn(() => 'anthropic'),
 45 | 	getResponseLanguage: jest.fn(() => 'English'),
 46 | 	getDefaultNumTasks: jest.fn(() => 10),
 47 | 	getDefaultSubtasks: jest.fn(() => 5),
 48 | 	getLogLevel: jest.fn(() => 'info'),
 49 | 	getConfig: jest.fn(() => ({})),
 50 | 	getAllProviders: jest.fn(() => ['anthropic', 'perplexity']),
 51 | 	MODEL_MAP: {},
 52 | 	VALID_PROVIDERS: ['anthropic', 'perplexity'],
 53 | 	validateProvider: jest.fn(() => true),
 54 | 	validateProviderModelCombination: jest.fn(() => true),
 55 | 	isApiKeySet: jest.fn(() => true),
 56 | 	hasCodebaseAnalysis: jest.fn(() => false)
 57 | }));
 58 | 
 59 | // Mock utils comprehensively to prevent CLI behavior
 60 | jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
 61 | 	log: jest.fn(),
 62 | 	writeJSON: jest.fn(),
 63 | 	enableSilentMode: jest.fn(),
 64 | 	disableSilentMode: jest.fn(),
 65 | 	isSilentMode: jest.fn(() => false),
 66 | 	getCurrentTag: jest.fn(() => 'master'),
 67 | 	ensureTagMetadata: jest.fn(),
 68 | 	readJSON: jest.fn(() => ({ master: { tasks: [] } })),
 69 | 	findProjectRoot: jest.fn(() => '/tmp/test'),
 70 | 	resolveEnvVariable: jest.fn(() => 'mock-key'),
 71 | 	findTaskById: jest.fn(() => null),
 72 | 	findTaskByPattern: jest.fn(() => []),
 73 | 	validateTaskId: jest.fn(() => true),
 74 | 	createTask: jest.fn(() => ({ id: 1, title: 'Mock Task' })),
 75 | 	sortByDependencies: jest.fn((tasks) => tasks),
 76 | 	isEmpty: jest.fn(() => false),
 77 | 	truncate: jest.fn((text) => text),
 78 | 	slugify: jest.fn((text) => text.toLowerCase()),
 79 | 	getTagFromPath: jest.fn(() => 'master'),
 80 | 	isValidTag: jest.fn(() => true),
 81 | 	migrateToTaggedFormat: jest.fn(() => ({ master: { tasks: [] } })),
 82 | 	performCompleteTagMigration: jest.fn(),
 83 | 	resolveCurrentTag: jest.fn(() => 'master'),
 84 | 	getDefaultTag: jest.fn(() => 'master'),
 85 | 	performMigrationIfNeeded: jest.fn()
 86 | }));
 87 | 
 88 | // Mock prompt manager
 89 | jest.unstable_mockModule('../../scripts/modules/prompt-manager.js', () => ({
 90 | 	getPromptManager: jest.fn(() => ({
 91 | 		loadPrompt: jest.fn(() => ({
 92 | 			systemPrompt: 'Test system prompt',
 93 | 			userPrompt: 'Test user prompt'
 94 | 		}))
 95 | 	}))
 96 | }));
 97 | 
 98 | // Mock progress/UI components to prevent real CLI UI
 99 | jest.unstable_mockModule('../../src/progress/parse-prd-tracker.js', () => ({
100 | 	createParsePrdTracker: jest.fn(() => ({
101 | 		start: jest.fn(),
102 | 		stop: jest.fn(),
103 | 		cleanup: jest.fn(),
104 | 		addTaskLine: jest.fn(),
105 | 		updateTokens: jest.fn(),
106 | 		complete: jest.fn(),
107 | 		getSummary: jest.fn().mockReturnValue({
108 | 			taskPriorities: { high: 0, medium: 0, low: 0 },
109 | 			elapsedTime: 0,
110 | 			actionVerb: 'generated'
111 | 		})
112 | 	}))
113 | }));
114 | 
115 | jest.unstable_mockModule('../../src/ui/parse-prd.js', () => ({
116 | 	displayParsePrdStart: jest.fn(),
117 | 	displayParsePrdSummary: jest.fn()
118 | }));
119 | 
120 | jest.unstable_mockModule('../../scripts/modules/ui.js', () => ({
121 | 	displayAiUsageSummary: jest.fn()
122 | }));
123 | 
124 | // Mock task generation to prevent file operations
125 | jest.unstable_mockModule(
126 | 	'../../scripts/modules/task-manager/generate-task-files.js',
127 | 	() => ({
128 | 		default: jest.fn()
129 | 	})
130 | );
131 | 
132 | // Mock stream parser
133 | jest.unstable_mockModule('../../src/utils/stream-parser.js', () => {
134 | 	// Define mock StreamingError class
135 | 	class StreamingError extends Error {
136 | 		constructor(message, code) {
137 | 			super(message);
138 | 			this.name = 'StreamingError';
139 | 			this.code = code;
140 | 		}
141 | 	}
142 | 
143 | 	// Define mock error codes
144 | 	const STREAMING_ERROR_CODES = {
145 | 		NOT_ASYNC_ITERABLE: 'STREAMING_NOT_SUPPORTED',
146 | 		STREAM_PROCESSING_FAILED: 'STREAM_PROCESSING_FAILED',
147 | 		STREAM_NOT_ITERABLE: 'STREAM_NOT_ITERABLE'
148 | 	};
149 | 
150 | 	return {
151 | 		parseStream: jest.fn(),
152 | 		StreamingError,
153 | 		STREAMING_ERROR_CODES
154 | 	};
155 | });
156 | 
157 | // Mock other potential UI elements
158 | jest.unstable_mockModule('ora', () => ({
159 | 	default: jest.fn(() => ({
160 | 		start: jest.fn(),
161 | 		stop: jest.fn(),
162 | 		succeed: jest.fn(),
163 | 		fail: jest.fn()
164 | 	}))
165 | }));
166 | 
167 | jest.unstable_mockModule('chalk', () => ({
168 | 	default: {
169 | 		red: jest.fn((text) => text),
170 | 		green: jest.fn((text) => text),
171 | 		blue: jest.fn((text) => text),
172 | 		yellow: jest.fn((text) => text),
173 | 		cyan: jest.fn((text) => text),
174 | 		white: {
175 | 			bold: jest.fn((text) => text)
176 | 		}
177 | 	},
178 | 	red: jest.fn((text) => text),
179 | 	green: jest.fn((text) => text),
180 | 	blue: jest.fn((text) => text),
181 | 	yellow: jest.fn((text) => text),
182 | 	cyan: jest.fn((text) => text),
183 | 	white: {
184 | 		bold: jest.fn((text) => text)
185 | 	}
186 | }));
187 | 
188 | // Mock boxen
189 | jest.unstable_mockModule('boxen', () => ({
190 | 	default: jest.fn((content) => content)
191 | }));
192 | 
193 | // Mock constants
194 | jest.unstable_mockModule('../../src/constants/task-priority.js', () => ({
195 | 	DEFAULT_TASK_PRIORITY: 'medium',
196 | 	TASK_PRIORITY_OPTIONS: ['low', 'medium', 'high']
197 | }));
198 | 
199 | // Mock UI indicators
200 | jest.unstable_mockModule('../../src/ui/indicators.js', () => ({
201 | 	getPriorityIndicators: jest.fn(() => ({
202 | 		high: '🔴',
203 | 		medium: '🟡',
204 | 		low: '🟢'
205 | 	}))
206 | }));
207 | 
208 | // Import modules after mocking
209 | const { generateObjectService } = await import(
210 | 	'../../scripts/modules/ai-services-unified.js'
211 | );
212 | const parsePRD = (
213 | 	await import('../../scripts/modules/task-manager/parse-prd/parse-prd.js')
214 | ).default;
215 | 
216 | describe('parse-prd file extension compatibility', () => {
217 | 	let tempDir;
218 | 	let testFiles;
219 | 
220 | 	const mockTasksResponse = {
221 | 		tasks: [
222 | 			{
223 | 				id: 1,
224 | 				title: 'Test Task 1',
225 | 				description: 'First test task',
226 | 				status: 'pending',
227 | 				dependencies: [],
228 | 				priority: 'high',
229 | 				details: 'Implementation details for task 1',
230 | 				testStrategy: 'Unit tests for task 1'
231 | 			},
232 | 			{
233 | 				id: 2,
234 | 				title: 'Test Task 2',
235 | 				description: 'Second test task',
236 | 				status: 'pending',
237 | 				dependencies: [1],
238 | 				priority: 'medium',
239 | 				details: 'Implementation details for task 2',
240 | 				testStrategy: 'Integration tests for task 2'
241 | 			}
242 | 		],
243 | 		metadata: {
244 | 			projectName: 'Test Project',
245 | 			totalTasks: 2,
246 | 			sourceFile: 'test-prd',
247 | 			generatedAt: new Date().toISOString()
248 | 		}
249 | 	};
250 | 
251 | 	const samplePRDContent = `# Test Project PRD
252 | 
253 | ## Overview
254 | Build a simple task management application.
255 | 
256 | ## Features
257 | 1. Create and manage tasks
258 | 2. Set task priorities
259 | 3. Track task dependencies
260 | 
261 | ## Technical Requirements
262 | - React frontend
263 | - Node.js backend
264 | - PostgreSQL database
265 | 
266 | ## Success Criteria
267 | - Users can create tasks successfully
268 | - Task dependencies work correctly`;
269 | 
270 | 	beforeAll(() => {
271 | 		// Create temporary directory for test files
272 | 		tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'parse-prd-test-'));
273 | 
274 | 		// Create test files with different extensions
275 | 		testFiles = {
276 | 			txt: path.join(tempDir, 'test-prd.txt'),
277 | 			md: path.join(tempDir, 'test-prd.md'),
278 | 			rst: path.join(tempDir, 'test-prd.rst'),
279 | 			noExt: path.join(tempDir, 'test-prd')
280 | 		};
281 | 
282 | 		// Write the same content to all test files
283 | 		Object.values(testFiles).forEach((filePath) => {
284 | 			fs.writeFileSync(filePath, samplePRDContent);
285 | 		});
286 | 
287 | 		// Mock process.exit to prevent actual exit
288 | 		jest.spyOn(process, 'exit').mockImplementation(() => undefined);
289 | 
290 | 		// Mock console methods to prevent output
291 | 		jest.spyOn(console, 'log').mockImplementation(() => {});
292 | 		jest.spyOn(console, 'error').mockImplementation(() => {});
293 | 	});
294 | 
295 | 	afterAll(() => {
296 | 		// Clean up temporary directory
297 | 		fs.rmSync(tempDir, { recursive: true, force: true });
298 | 
299 | 		// Restore mocks
300 | 		jest.restoreAllMocks();
301 | 	});
302 | 
303 | 	beforeEach(() => {
304 | 		jest.clearAllMocks();
305 | 
306 | 		// Mock successful AI response
307 | 		generateObjectService.mockResolvedValue({
308 | 			mainResult: { object: mockTasksResponse },
309 | 			telemetryData: {
310 | 				timestamp: new Date().toISOString(),
311 | 				userId: 'test-user',
312 | 				commandName: 'parse-prd',
313 | 				modelUsed: 'test-model',
314 | 				providerName: 'test-provider',
315 | 				inputTokens: 100,
316 | 				outputTokens: 200,
317 | 				totalTokens: 300,
318 | 				totalCost: 0.01,
319 | 				currency: 'USD'
320 | 			}
321 | 		});
322 | 	});
323 | 
324 | 	test('should accept and parse .txt files', async () => {
325 | 		const outputPath = path.join(tempDir, 'tasks-txt.json');
326 | 
327 | 		const result = await parsePRD(testFiles.txt, outputPath, 2, {
328 | 			force: true,
329 | 			mcpLog: {
330 | 				info: jest.fn(),
331 | 				warn: jest.fn(),
332 | 				error: jest.fn(),
333 | 				debug: jest.fn(),
334 | 				success: jest.fn()
335 | 			},
336 | 			projectRoot: tempDir
337 | 		});
338 | 
339 | 		expect(result.success).toBe(true);
340 | 		expect(result.tasksPath).toBe(outputPath);
341 | 		expect(fs.existsSync(outputPath)).toBe(true);
342 | 
343 | 		// Verify the content was parsed correctly
344 | 		const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
345 | 		expect(tasksData.master.tasks).toHaveLength(2);
346 | 		expect(tasksData.master.tasks[0].title).toBe('Test Task 1');
347 | 	});
348 | 
349 | 	test('should accept and parse .md files', async () => {
350 | 		const outputPath = path.join(tempDir, 'tasks-md.json');
351 | 
352 | 		const result = await parsePRD(testFiles.md, outputPath, 2, {
353 | 			force: true,
354 | 			mcpLog: {
355 | 				info: jest.fn(),
356 | 				warn: jest.fn(),
357 | 				error: jest.fn(),
358 | 				debug: jest.fn(),
359 | 				success: jest.fn()
360 | 			},
361 | 			projectRoot: tempDir
362 | 		});
363 | 
364 | 		expect(result.success).toBe(true);
365 | 		expect(result.tasksPath).toBe(outputPath);
366 | 		expect(fs.existsSync(outputPath)).toBe(true);
367 | 
368 | 		// Verify the content was parsed correctly
369 | 		const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
370 | 		expect(tasksData.master.tasks).toHaveLength(2);
371 | 	});
372 | 
373 | 	test('should accept and parse files with other text extensions', async () => {
374 | 		const outputPath = path.join(tempDir, 'tasks-rst.json');
375 | 
376 | 		const result = await parsePRD(testFiles.rst, outputPath, 2, {
377 | 			force: true,
378 | 			mcpLog: {
379 | 				info: jest.fn(),
380 | 				warn: jest.fn(),
381 | 				error: jest.fn(),
382 | 				debug: jest.fn(),
383 | 				success: jest.fn()
384 | 			},
385 | 			projectRoot: tempDir
386 | 		});
387 | 
388 | 		expect(result.success).toBe(true);
389 | 		expect(result.tasksPath).toBe(outputPath);
390 | 		expect(fs.existsSync(outputPath)).toBe(true);
391 | 	});
392 | 
393 | 	test('should accept and parse files with no extension', async () => {
394 | 		const outputPath = path.join(tempDir, 'tasks-noext.json');
395 | 
396 | 		const result = await parsePRD(testFiles.noExt, outputPath, 2, {
397 | 			force: true,
398 | 			mcpLog: {
399 | 				info: jest.fn(),
400 | 				warn: jest.fn(),
401 | 				error: jest.fn(),
402 | 				debug: jest.fn(),
403 | 				success: jest.fn()
404 | 			},
405 | 			projectRoot: tempDir
406 | 		});
407 | 
408 | 		expect(result.success).toBe(true);
409 | 		expect(result.tasksPath).toBe(outputPath);
410 | 		expect(fs.existsSync(outputPath)).toBe(true);
411 | 	});
412 | 
413 | 	test('should produce identical results regardless of file extension', async () => {
414 | 		const outputs = {};
415 | 
416 | 		// Parse each file type with a unique project root to avoid ID conflicts
417 | 		for (const [ext, filePath] of Object.entries(testFiles)) {
418 | 			// Create a unique subdirectory for each test to isolate them
419 | 			const testSubDir = path.join(tempDir, `test-${ext}`);
420 | 			fs.mkdirSync(testSubDir, { recursive: true });
421 | 
422 | 			const outputPath = path.join(testSubDir, `tasks.json`);
423 | 
424 | 			await parsePRD(filePath, outputPath, 2, {
425 | 				force: true,
426 | 				mcpLog: {
427 | 					info: jest.fn(),
428 | 					warn: jest.fn(),
429 | 					error: jest.fn(),
430 | 					debug: jest.fn(),
431 | 					success: jest.fn()
432 | 				},
433 | 				projectRoot: testSubDir
434 | 			});
435 | 
436 | 			const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
437 | 			outputs[ext] = tasksData;
438 | 		}
439 | 
440 | 		// Compare all outputs - they should be identical (except metadata timestamps)
441 | 		const baseOutput = outputs.txt;
442 | 		Object.values(outputs).forEach((output) => {
443 | 			expect(output.master.tasks).toEqual(baseOutput.master.tasks);
444 | 			expect(output.master.metadata.projectName).toEqual(
445 | 				baseOutput.master.metadata.projectName
446 | 			);
447 | 			expect(output.master.metadata.totalTasks).toEqual(
448 | 				baseOutput.master.metadata.totalTasks
449 | 			);
450 | 		});
451 | 	});
452 | 
453 | 	test('should handle non-existent files gracefully', async () => {
454 | 		const nonExistentFile = path.join(tempDir, 'does-not-exist.txt');
455 | 		const outputPath = path.join(tempDir, 'tasks-error.json');
456 | 
457 | 		await expect(
458 | 			parsePRD(nonExistentFile, outputPath, 2, {
459 | 				force: true,
460 | 				mcpLog: {
461 | 					info: jest.fn(),
462 | 					warn: jest.fn(),
463 | 					error: jest.fn(),
464 | 					debug: jest.fn(),
465 | 					success: jest.fn()
466 | 				},
467 | 				projectRoot: tempDir
468 | 			})
469 | 		).rejects.toThrow();
470 | 	});
471 | });
472 | 
```

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

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

--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/expand-all-tasks.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Tests for the expand-all-tasks.js module
  3 |  */
  4 | import { jest } from '@jest/globals';
  5 | 
  6 | // Mock the dependencies before importing the module under test
  7 | jest.unstable_mockModule(
  8 | 	'../../../../../scripts/modules/task-manager/expand-task.js',
  9 | 	() => ({
 10 | 		default: jest.fn()
 11 | 	})
 12 | );
 13 | 
 14 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
 15 | 	readJSON: jest.fn(),
 16 | 	log: jest.fn(),
 17 | 	isSilentMode: jest.fn(() => false),
 18 | 	findProjectRoot: jest.fn(() => '/test/project'),
 19 | 	aggregateTelemetry: jest.fn()
 20 | }));
 21 | 
 22 | jest.unstable_mockModule(
 23 | 	'../../../../../scripts/modules/config-manager.js',
 24 | 	() => ({
 25 | 		getDebugFlag: jest.fn(() => false)
 26 | 	})
 27 | );
 28 | 
 29 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
 30 | 	startLoadingIndicator: jest.fn(),
 31 | 	stopLoadingIndicator: jest.fn(),
 32 | 	displayAiUsageSummary: jest.fn()
 33 | }));
 34 | 
 35 | jest.unstable_mockModule('chalk', () => ({
 36 | 	default: {
 37 | 		white: { bold: jest.fn((text) => text) },
 38 | 		cyan: jest.fn((text) => text),
 39 | 		green: jest.fn((text) => text),
 40 | 		gray: jest.fn((text) => text),
 41 | 		red: jest.fn((text) => text),
 42 | 		bold: jest.fn((text) => text)
 43 | 	}
 44 | }));
 45 | 
 46 | jest.unstable_mockModule('boxen', () => ({
 47 | 	default: jest.fn((text) => text)
 48 | }));
 49 | 
 50 | // Import the mocked modules
 51 | const { default: expandTask } = await import(
 52 | 	'../../../../../scripts/modules/task-manager/expand-task.js'
 53 | );
 54 | const { readJSON, aggregateTelemetry, findProjectRoot } = await import(
 55 | 	'../../../../../scripts/modules/utils.js'
 56 | );
 57 | 
 58 | // Import the module under test
 59 | const { default: expandAllTasks } = await import(
 60 | 	'../../../../../scripts/modules/task-manager/expand-all-tasks.js'
 61 | );
 62 | 
 63 | const mockExpandTask = expandTask;
 64 | const mockReadJSON = readJSON;
 65 | const mockAggregateTelemetry = aggregateTelemetry;
 66 | const mockFindProjectRoot = findProjectRoot;
 67 | 
 68 | describe('expandAllTasks', () => {
 69 | 	const mockTasksPath = '/test/tasks.json';
 70 | 	const mockProjectRoot = '/test/project';
 71 | 	const mockSession = { userId: 'test-user' };
 72 | 	const mockMcpLog = {
 73 | 		info: jest.fn(),
 74 | 		warn: jest.fn(),
 75 | 		error: jest.fn(),
 76 | 		debug: jest.fn()
 77 | 	};
 78 | 
 79 | 	const sampleTasksData = {
 80 | 		tag: 'master',
 81 | 		tasks: [
 82 | 			{
 83 | 				id: 1,
 84 | 				title: 'Pending Task 1',
 85 | 				status: 'pending',
 86 | 				subtasks: []
 87 | 			},
 88 | 			{
 89 | 				id: 2,
 90 | 				title: 'In Progress Task',
 91 | 				status: 'in-progress',
 92 | 				subtasks: []
 93 | 			},
 94 | 			{
 95 | 				id: 3,
 96 | 				title: 'Done Task',
 97 | 				status: 'done',
 98 | 				subtasks: []
 99 | 			},
100 | 			{
101 | 				id: 4,
102 | 				title: 'Task with Subtasks',
103 | 				status: 'pending',
104 | 				subtasks: [{ id: '4.1', title: 'Existing subtask' }]
105 | 			}
106 | 		]
107 | 	};
108 | 
109 | 	beforeEach(() => {
110 | 		jest.clearAllMocks();
111 | 		mockReadJSON.mockReturnValue(sampleTasksData);
112 | 		mockAggregateTelemetry.mockReturnValue({
113 | 			timestamp: '2024-01-01T00:00:00.000Z',
114 | 			commandName: 'expand-all-tasks',
115 | 			totalCost: 0.1,
116 | 			totalTokens: 2000,
117 | 			inputTokens: 1200,
118 | 			outputTokens: 800
119 | 		});
120 | 	});
121 | 
122 | 	describe('successful expansion', () => {
123 | 		test('should expand all eligible pending tasks', async () => {
124 | 			// Arrange
125 | 			const mockTelemetryData = {
126 | 				timestamp: '2024-01-01T00:00:00.000Z',
127 | 				commandName: 'expand-task',
128 | 				totalCost: 0.05,
129 | 				totalTokens: 1000
130 | 			};
131 | 
132 | 			mockExpandTask.mockResolvedValue({
133 | 				telemetryData: mockTelemetryData
134 | 			});
135 | 
136 | 			// Act
137 | 			const result = await expandAllTasks(
138 | 				mockTasksPath,
139 | 				3, // numSubtasks
140 | 				false, // useResearch
141 | 				'test context', // additionalContext
142 | 				false, // force
143 | 				{
144 | 					session: mockSession,
145 | 					mcpLog: mockMcpLog,
146 | 					projectRoot: mockProjectRoot,
147 | 					tag: 'master'
148 | 				},
149 | 				'json' // outputFormat
150 | 			);
151 | 
152 | 			// Assert
153 | 			expect(result.success).toBe(true);
154 | 			expect(result.expandedCount).toBe(2); // Tasks 1 and 2 (pending and in-progress)
155 | 			expect(result.failedCount).toBe(0);
156 | 			expect(result.skippedCount).toBe(0);
157 | 			expect(result.tasksToExpand).toBe(2);
158 | 			expect(result.telemetryData).toBeDefined();
159 | 
160 | 			// Verify readJSON was called correctly
161 | 			expect(mockReadJSON).toHaveBeenCalledWith(
162 | 				mockTasksPath,
163 | 				mockProjectRoot,
164 | 				'master'
165 | 			);
166 | 
167 | 			// Verify expandTask was called for eligible tasks
168 | 			expect(mockExpandTask).toHaveBeenCalledTimes(2);
169 | 			expect(mockExpandTask).toHaveBeenCalledWith(
170 | 				mockTasksPath,
171 | 				1,
172 | 				3,
173 | 				false,
174 | 				'test context',
175 | 				expect.objectContaining({
176 | 					session: mockSession,
177 | 					mcpLog: mockMcpLog,
178 | 					projectRoot: mockProjectRoot,
179 | 					tag: 'master'
180 | 				}),
181 | 				false
182 | 			);
183 | 		});
184 | 
185 | 		test('should handle force flag to expand tasks with existing subtasks', async () => {
186 | 			// Arrange
187 | 			mockExpandTask.mockResolvedValue({
188 | 				telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
189 | 			});
190 | 
191 | 			// Act
192 | 			const result = await expandAllTasks(
193 | 				mockTasksPath,
194 | 				2,
195 | 				false,
196 | 				'',
197 | 				true, // force = true
198 | 				{
199 | 					session: mockSession,
200 | 					mcpLog: mockMcpLog,
201 | 					projectRoot: mockProjectRoot,
202 | 					tag: 'master'
203 | 				},
204 | 				'json'
205 | 			);
206 | 
207 | 			// Assert
208 | 			expect(result.expandedCount).toBe(3); // Tasks 1, 2, and 4 (including task with existing subtasks)
209 | 			expect(mockExpandTask).toHaveBeenCalledTimes(3);
210 | 		});
211 | 
212 | 		test('should handle research flag', async () => {
213 | 			// Arrange
214 | 			mockExpandTask.mockResolvedValue({
215 | 				telemetryData: { commandName: 'expand-task', totalCost: 0.08 }
216 | 			});
217 | 
218 | 			// Act
219 | 			const result = await expandAllTasks(
220 | 				mockTasksPath,
221 | 				undefined, // numSubtasks not specified
222 | 				true, // useResearch = true
223 | 				'research context',
224 | 				false,
225 | 				{
226 | 					session: mockSession,
227 | 					mcpLog: mockMcpLog,
228 | 					projectRoot: mockProjectRoot,
229 | 					tag: 'master'
230 | 				},
231 | 				'json'
232 | 			);
233 | 
234 | 			// Assert
235 | 			expect(result.success).toBe(true);
236 | 			expect(mockExpandTask).toHaveBeenCalledWith(
237 | 				mockTasksPath,
238 | 				expect.any(Number),
239 | 				undefined,
240 | 				true, // research flag passed correctly
241 | 				'research context',
242 | 				expect.any(Object),
243 | 				false
244 | 			);
245 | 		});
246 | 
247 | 		test('should pass complexityReportPath to expandTask when provided in context', async () => {
248 | 			// Arrange
249 | 			const mockComplexityReportPath =
250 | 				'/test/project/.taskmaster/reports/task-complexity-report.json';
251 | 			mockExpandTask.mockResolvedValue({
252 | 				telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
253 | 			});
254 | 
255 | 			// Act
256 | 			const result = await expandAllTasks(
257 | 				mockTasksPath,
258 | 				undefined, // numSubtasks not specified, should use complexity report
259 | 				false,
260 | 				'',
261 | 				false,
262 | 				{
263 | 					session: mockSession,
264 | 					mcpLog: mockMcpLog,
265 | 					projectRoot: mockProjectRoot,
266 | 					tag: 'master',
267 | 					complexityReportPath: mockComplexityReportPath
268 | 				},
269 | 				'json'
270 | 			);
271 | 
272 | 			// Assert
273 | 			expect(result.success).toBe(true);
274 | 			expect(result.expandedCount).toBe(2); // Tasks 1 and 2
275 | 
276 | 			// Verify expandTask was called with complexityReportPath in context
277 | 			expect(mockExpandTask).toHaveBeenCalledWith(
278 | 				mockTasksPath,
279 | 				expect.any(Number), // task id
280 | 				undefined, // numSubtasks
281 | 				false, // useResearch
282 | 				'', // additionalContext
283 | 				expect.objectContaining({
284 | 					session: mockSession,
285 | 					mcpLog: mockMcpLog,
286 | 					projectRoot: mockProjectRoot,
287 | 					tag: 'master',
288 | 					complexityReportPath: mockComplexityReportPath
289 | 				}),
290 | 				false // force
291 | 			);
292 | 		});
293 | 
294 | 		test('should return success with message when no tasks are eligible', async () => {
295 | 			// Arrange - Mock tasks data with no eligible tasks
296 | 			const noEligibleTasksData = {
297 | 				tag: 'master',
298 | 				tasks: [
299 | 					{ id: 1, status: 'done', subtasks: [] },
300 | 					{
301 | 						id: 2,
302 | 						status: 'pending',
303 | 						subtasks: [{ id: '2.1', title: 'existing' }]
304 | 					}
305 | 				]
306 | 			};
307 | 			mockReadJSON.mockReturnValue(noEligibleTasksData);
308 | 
309 | 			// Act
310 | 			const result = await expandAllTasks(
311 | 				mockTasksPath,
312 | 				3,
313 | 				false,
314 | 				'',
315 | 				false, // force = false, so task with subtasks won't be expanded
316 | 				{
317 | 					session: mockSession,
318 | 					mcpLog: mockMcpLog,
319 | 					projectRoot: mockProjectRoot,
320 | 					tag: 'master'
321 | 				},
322 | 				'json'
323 | 			);
324 | 
325 | 			// Assert
326 | 			expect(result.success).toBe(true);
327 | 			expect(result.expandedCount).toBe(0);
328 | 			expect(result.failedCount).toBe(0);
329 | 			expect(result.skippedCount).toBe(0);
330 | 			expect(result.tasksToExpand).toBe(0);
331 | 			expect(result.message).toBe('No tasks eligible for expansion.');
332 | 			expect(mockExpandTask).not.toHaveBeenCalled();
333 | 		});
334 | 	});
335 | 
336 | 	describe('error handling', () => {
337 | 		test('should handle expandTask failures gracefully', async () => {
338 | 			// Arrange
339 | 			mockExpandTask
340 | 				.mockResolvedValueOnce({ telemetryData: { totalCost: 0.05 } }) // First task succeeds
341 | 				.mockRejectedValueOnce(new Error('AI service error')); // Second task fails
342 | 
343 | 			// Act
344 | 			const result = await expandAllTasks(
345 | 				mockTasksPath,
346 | 				3,
347 | 				false,
348 | 				'',
349 | 				false,
350 | 				{
351 | 					session: mockSession,
352 | 					mcpLog: mockMcpLog,
353 | 					projectRoot: mockProjectRoot,
354 | 					tag: 'master'
355 | 				},
356 | 				'json'
357 | 			);
358 | 
359 | 			// Assert
360 | 			expect(result.success).toBe(true);
361 | 			expect(result.expandedCount).toBe(1);
362 | 			expect(result.failedCount).toBe(1);
363 | 		});
364 | 
365 | 		test('should throw error when tasks.json is invalid', async () => {
366 | 			// Arrange
367 | 			mockReadJSON.mockReturnValue(null);
368 | 
369 | 			// Act & Assert
370 | 			await expect(
371 | 				expandAllTasks(
372 | 					mockTasksPath,
373 | 					3,
374 | 					false,
375 | 					'',
376 | 					false,
377 | 					{
378 | 						session: mockSession,
379 | 						mcpLog: mockMcpLog,
380 | 						projectRoot: mockProjectRoot,
381 | 						tag: 'master'
382 | 					},
383 | 					'json'
384 | 				)
385 | 			).rejects.toThrow('Invalid tasks data');
386 | 		});
387 | 
388 | 		test('should throw error when project root cannot be determined', async () => {
389 | 			// Arrange - Mock findProjectRoot to return null for this test
390 | 			mockFindProjectRoot.mockReturnValueOnce(null);
391 | 
392 | 			// Act & Assert
393 | 			await expect(
394 | 				expandAllTasks(
395 | 					mockTasksPath,
396 | 					3,
397 | 					false,
398 | 					'',
399 | 					false,
400 | 					{
401 | 						session: mockSession,
402 | 						mcpLog: mockMcpLog,
403 | 						tag: 'master'
404 | 						// No projectRoot provided, and findProjectRoot will return null
405 | 					},
406 | 					'json'
407 | 				)
408 | 			).rejects.toThrow('Could not determine project root directory');
409 | 		});
410 | 	});
411 | 
412 | 	describe('telemetry aggregation', () => {
413 | 		test('should aggregate telemetry data from multiple expand operations', async () => {
414 | 			// Arrange
415 | 			const telemetryData1 = {
416 | 				commandName: 'expand-task',
417 | 				totalCost: 0.03,
418 | 				totalTokens: 600
419 | 			};
420 | 			const telemetryData2 = {
421 | 				commandName: 'expand-task',
422 | 				totalCost: 0.04,
423 | 				totalTokens: 800
424 | 			};
425 | 
426 | 			mockExpandTask
427 | 				.mockResolvedValueOnce({ telemetryData: telemetryData1 })
428 | 				.mockResolvedValueOnce({ telemetryData: telemetryData2 });
429 | 
430 | 			// Act
431 | 			const result = await expandAllTasks(
432 | 				mockTasksPath,
433 | 				3,
434 | 				false,
435 | 				'',
436 | 				false,
437 | 				{
438 | 					session: mockSession,
439 | 					mcpLog: mockMcpLog,
440 | 					projectRoot: mockProjectRoot,
441 | 					tag: 'master'
442 | 				},
443 | 				'json'
444 | 			);
445 | 
446 | 			// Assert
447 | 			expect(mockAggregateTelemetry).toHaveBeenCalledWith(
448 | 				[telemetryData1, telemetryData2],
449 | 				'expand-all-tasks'
450 | 			);
451 | 			expect(result.telemetryData).toBeDefined();
452 | 			expect(result.telemetryData.commandName).toBe('expand-all-tasks');
453 | 		});
454 | 
455 | 		test('should handle missing telemetry data gracefully', async () => {
456 | 			// Arrange
457 | 			mockExpandTask.mockResolvedValue({}); // No telemetryData
458 | 
459 | 			// Act
460 | 			const result = await expandAllTasks(
461 | 				mockTasksPath,
462 | 				3,
463 | 				false,
464 | 				'',
465 | 				false,
466 | 				{
467 | 					session: mockSession,
468 | 					mcpLog: mockMcpLog,
469 | 					projectRoot: mockProjectRoot,
470 | 					tag: 'master'
471 | 				},
472 | 				'json'
473 | 			);
474 | 
475 | 			// Assert
476 | 			expect(result.success).toBe(true);
477 | 			expect(mockAggregateTelemetry).toHaveBeenCalledWith(
478 | 				[],
479 | 				'expand-all-tasks'
480 | 			);
481 | 		});
482 | 	});
483 | 
484 | 	describe('output format handling', () => {
485 | 		test('should use text output format for CLI calls', async () => {
486 | 			// Arrange
487 | 			mockExpandTask.mockResolvedValue({
488 | 				telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
489 | 			});
490 | 
491 | 			// Act
492 | 			const result = await expandAllTasks(
493 | 				mockTasksPath,
494 | 				3,
495 | 				false,
496 | 				'',
497 | 				false,
498 | 				{
499 | 					projectRoot: mockProjectRoot,
500 | 					tag: 'master'
501 | 					// No mcpLog provided, should use CLI logger
502 | 				},
503 | 				'text' // CLI output format
504 | 			);
505 | 
506 | 			// Assert
507 | 			expect(result.success).toBe(true);
508 | 			// In text mode, loading indicators and console output would be used
509 | 			// This is harder to test directly but we can verify the result structure
510 | 		});
511 | 
512 | 		test('should handle context tag properly', async () => {
513 | 			// Arrange
514 | 			const taggedTasksData = {
515 | 				...sampleTasksData,
516 | 				tag: 'feature-branch'
517 | 			};
518 | 			mockReadJSON.mockReturnValue(taggedTasksData);
519 | 			mockExpandTask.mockResolvedValue({
520 | 				telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
521 | 			});
522 | 
523 | 			// Act
524 | 			const result = await expandAllTasks(
525 | 				mockTasksPath,
526 | 				3,
527 | 				false,
528 | 				'',
529 | 				false,
530 | 				{
531 | 					session: mockSession,
532 | 					mcpLog: mockMcpLog,
533 | 					projectRoot: mockProjectRoot,
534 | 					tag: 'feature-branch'
535 | 				},
536 | 				'json'
537 | 			);
538 | 
539 | 			// Assert
540 | 			expect(mockReadJSON).toHaveBeenCalledWith(
541 | 				mockTasksPath,
542 | 				mockProjectRoot,
543 | 				'feature-branch'
544 | 			);
545 | 			expect(mockExpandTask).toHaveBeenCalledWith(
546 | 				mockTasksPath,
547 | 				expect.any(Number),
548 | 				3,
549 | 				false,
550 | 				'',
551 | 				expect.objectContaining({
552 | 					tag: 'feature-branch'
553 | 				}),
554 | 				false
555 | 			);
556 | 		});
557 | 	});
558 | });
559 | 
```
Page 35/69FirstPrevNextLast