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

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/tasks/tasks-domain.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Tasks Domain Facade
  3 |  * Public API for task-related operations
  4 |  */
  5 | 
  6 | import type { ConfigManager } from '../config/managers/config-manager.js';
  7 | import type { AuthDomain } from '../auth/auth-domain.js';
  8 | import { BriefsDomain } from '../briefs/briefs-domain.js';
  9 | import { TaskService } from './services/task-service.js';
 10 | import { TaskExecutionService } from './services/task-execution-service.js';
 11 | import { TaskLoaderService } from './services/task-loader.service.js';
 12 | import { PreflightChecker } from './services/preflight-checker.service.js';
 13 | import { TagService } from './services/tag.service.js';
 14 | import type {
 15 | 	CreateTagOptions,
 16 | 	DeleteTagOptions,
 17 | 	CopyTagOptions
 18 | } from './services/tag.service.js';
 19 | 
 20 | import type { Subtask, Task, TaskStatus } from '../../common/types/index.js';
 21 | import type {
 22 | 	TaskListResult,
 23 | 	GetTaskListOptions
 24 | } from './services/task-service.js';
 25 | import type {
 26 | 	StartTaskOptions,
 27 | 	StartTaskResult
 28 | } from './services/task-execution-service.js';
 29 | import type {
 30 | 	PreflightResult
 31 | } from './services/preflight-checker.service.js';
 32 | import type { TaskValidationResult } from './services/task-loader.service.js';
 33 | import type { ExpandTaskResult } from '../integration/services/task-expansion.service.js';
 34 | 
 35 | /**
 36 |  * Tasks Domain - Unified API for all task operations
 37 |  */
 38 | export class TasksDomain {
 39 | 	private taskService: TaskService;
 40 | 	private executionService: TaskExecutionService;
 41 | 	private loaderService: TaskLoaderService;
 42 | 	private preflightChecker: PreflightChecker;
 43 | 	private briefsDomain: BriefsDomain;
 44 | 	private tagService!: TagService;
 45 | 
 46 | 	constructor(configManager: ConfigManager, _authDomain?: AuthDomain) {
 47 | 		this.taskService = new TaskService(configManager);
 48 | 		this.executionService = new TaskExecutionService(this.taskService);
 49 | 		this.loaderService = new TaskLoaderService(this.taskService);
 50 | 		this.preflightChecker = new PreflightChecker(configManager.getProjectRoot());
 51 | 		this.briefsDomain = new BriefsDomain();
 52 | 	}
 53 | 
 54 | 	async initialize(): Promise<void> {
 55 | 		await this.taskService.initialize();
 56 | 
 57 | 		// TagService needs storage - get it from TaskService AFTER initialization
 58 | 		this.tagService = new TagService(this.taskService.getStorage());
 59 | 	}
 60 | 
 61 | 	// ========== Task Retrieval ==========
 62 | 
 63 | 	/**
 64 | 	 * Get list of tasks with filtering
 65 | 	 */
 66 | 	async list(options?: GetTaskListOptions): Promise<TaskListResult> {
 67 | 		return this.taskService.getTaskList(options);
 68 | 	}
 69 | 
 70 | 	/**
 71 | 	 * Get a single task by ID
 72 | 	 * Automatically handles all ID formats:
 73 | 	 * - Simple task IDs (e.g., "1", "HAM-123")
 74 | 	 * - Subtask IDs with dot notation (e.g., "1.2", "HAM-123.2")
 75 | 	 *
 76 | 	 * @returns Discriminated union indicating task/subtask with proper typing
 77 | 	 */
 78 | 	async get(
 79 | 		taskId: string,
 80 | 		tag?: string
 81 | 	): Promise<
 82 | 		| { task: Task; isSubtask: false }
 83 | 		| { task: Subtask; isSubtask: true }
 84 | 		| { task: null; isSubtask: boolean }
 85 | 	> {
 86 | 		// Parse ID - check for dot notation (subtask)
 87 | 		const parts = taskId.split('.');
 88 | 		const parentId = parts[0];
 89 | 		const subtaskIdPart = parts[1];
 90 | 
 91 | 		// Fetch the task
 92 | 		const task = await this.taskService.getTask(parentId, tag);
 93 | 		if (!task) {
 94 | 			return { task: null, isSubtask: false };
 95 | 		}
 96 | 
 97 | 		// Handle subtask notation (1.2)
 98 | 		if (subtaskIdPart && task.subtasks) {
 99 | 			const subtask = task.subtasks.find(
100 | 				(st) => String(st.id) === subtaskIdPart
101 | 			);
102 | 			if (subtask) {
103 | 				// Return the actual subtask with properly typed result
104 | 				return { task: subtask, isSubtask: true };
105 | 			}
106 | 			// Subtask ID provided but not found
107 | 			return { task: null, isSubtask: true };
108 | 		}
109 | 
110 | 		// It's a regular task
111 | 		return { task, isSubtask: false };
112 | 	}
113 | 
114 | 	/**
115 | 	 * Get tasks by status
116 | 	 */
117 | 	async getByStatus(status: TaskStatus, tag?: string): Promise<Task[]> {
118 | 		return this.taskService.getTasksByStatus(status, tag);
119 | 	}
120 | 
121 | 	/**
122 | 	 * Get task statistics
123 | 	 */
124 | 	async getStats(tag?: string) {
125 | 		return this.taskService.getTaskStats(tag);
126 | 	}
127 | 
128 | 	/**
129 | 	 * Get next available task to work on
130 | 	 */
131 | 	async getNext(tag?: string): Promise<Task | null> {
132 | 		return this.taskService.getNextTask(tag);
133 | 	}
134 | 
135 | 	// ========== Task Status Management ==========
136 | 
137 | 	/**
138 | 	 * Update task with new data (direct structural update)
139 | 	 * @param taskId - Task ID (supports numeric, alphanumeric like TAS-49, and subtask IDs like 1.2)
140 | 	 * @param updates - Partial task object with fields to update
141 | 	 * @param tag - Optional tag context
142 | 	 */
143 | 	async update(
144 | 		taskId: string | number,
145 | 		updates: Partial<Task>,
146 | 		tag?: string
147 | 	): Promise<void> {
148 | 		return this.taskService.updateTask(taskId, updates, tag);
149 | 	}
150 | 
151 | 	/**
152 | 	 * Update task using AI-powered prompt (natural language update)
153 | 	 * @param taskId - Task ID (supports numeric, alphanumeric like TAS-49, and subtask IDs like 1.2)
154 | 	 * @param prompt - Natural language prompt describing the update
155 | 	 * @param tag - Optional tag context
156 | 	 * @param options - Optional update options
157 | 	 * @param options.useResearch - Use research AI for file storage updates
158 | 	 * @param options.mode - Update mode for API storage: 'append', 'update', or 'rewrite'
159 | 	 */
160 | 	async updateWithPrompt(
161 | 		taskId: string | number,
162 | 		prompt: string,
163 | 		tag?: string,
164 | 		options?: { mode?: 'append' | 'update' | 'rewrite'; useResearch?: boolean }
165 | 	): Promise<void> {
166 | 		return this.taskService.updateTaskWithPrompt(taskId, prompt, tag, options);
167 | 	}
168 | 
169 | 	/**
170 | 	 * Expand task into subtasks using AI
171 | 	 * @returns ExpandTaskResult when using API storage, void for file storage
172 | 	 */
173 | 	async expand(
174 | 		taskId: string | number,
175 | 		tag?: string,
176 | 		options?: {
177 | 			numSubtasks?: number;
178 | 			useResearch?: boolean;
179 | 			additionalContext?: string;
180 | 			force?: boolean;
181 | 		}
182 | 	): Promise<ExpandTaskResult | void> {
183 | 		return this.taskService.expandTaskWithPrompt(taskId, tag, options);
184 | 	}
185 | 
186 | 	/**
187 | 	 * Update task status
188 | 	 */
189 | 	async updateStatus(taskId: string, status: TaskStatus, tag?: string) {
190 | 		return this.taskService.updateTaskStatus(taskId, status, tag);
191 | 	}
192 | 
193 | 	/**
194 | 	 * Set active tag
195 | 	 */
196 | 	async setActiveTag(tag: string): Promise<void> {
197 | 		return this.taskService.setActiveTag(tag);
198 | 	}
199 | 
200 | 	/**
201 | 	 * Resolve a brief by ID, name, or partial match without switching
202 | 	 * Returns the full brief object
203 | 	 *
204 | 	 * Supports:
205 | 	 * - Full UUID
206 | 	 * - Last 8 characters of UUID
207 | 	 * - Brief name (exact or partial match)
208 | 	 *
209 | 	 * Only works with API storage (briefs).
210 | 	 *
211 | 	 * @param briefIdOrName - Brief identifier
212 | 	 * @param orgId - Optional organization ID
213 | 	 * @returns The resolved brief object
214 | 	 */
215 | 	async resolveBrief(briefIdOrName: string, orgId?: string): Promise<any> {
216 | 		return this.briefsDomain.resolveBrief(briefIdOrName, orgId);
217 | 	}
218 | 
219 | 	/**
220 | 	 * Switch to a different tag/brief context
221 | 	 * For file storage: updates active tag in state
222 | 	 * For API storage: looks up brief by name and updates auth context
223 | 	 */
224 | 	async switchTag(tagName: string): Promise<void> {
225 | 		const storageType = this.taskService.getStorageType();
226 | 
227 | 		if (storageType === 'file') {
228 | 			await this.setActiveTag(tagName);
229 | 		} else {
230 | 			await this.briefsDomain.switchBrief(tagName);
231 | 		}
232 | 	}
233 | 
234 | 	// ========== Task Execution ==========
235 | 
236 | 	/**
237 | 	 * Start working on a task
238 | 	 */
239 | 	async start(taskId: string, options?: StartTaskOptions): Promise<StartTaskResult> {
240 | 		return this.executionService.startTask(taskId, options);
241 | 	}
242 | 
243 | 	/**
244 | 	 * Check for in-progress conflicts
245 | 	 */
246 | 	async checkInProgressConflicts(taskId: string) {
247 | 		return this.executionService.checkInProgressConflicts(taskId);
248 | 	}
249 | 
250 | 	/**
251 | 	 * Get next available task (from execution service)
252 | 	 */
253 | 	async getNextAvailable(): Promise<string | null> {
254 | 		return this.executionService.getNextAvailableTask();
255 | 	}
256 | 
257 | 	/**
258 | 	 * Check if a task can be started
259 | 	 */
260 | 	async canStart(taskId: string, force?: boolean): Promise<boolean> {
261 | 		return this.executionService.canStartTask(taskId, force);
262 | 	}
263 | 
264 | 	// ========== Task Loading & Validation ==========
265 | 
266 | 	/**
267 | 	 * Load and validate a task for execution
268 | 	 */
269 | 	async loadAndValidate(taskId: string): Promise<TaskValidationResult> {
270 | 		return this.loaderService.loadAndValidateTask(taskId);
271 | 	}
272 | 
273 | 	/**
274 | 	 * Get execution order for subtasks
275 | 	 */
276 | 	getExecutionOrder(task: Task) {
277 | 		return this.loaderService.getExecutionOrder(task);
278 | 	}
279 | 
280 | 	// ========== Preflight Checks ==========
281 | 
282 | 	/**
283 | 	 * Run all preflight checks
284 | 	 */
285 | 	async runPreflightChecks(): Promise<PreflightResult> {
286 | 		return this.preflightChecker.runAllChecks();
287 | 	}
288 | 
289 | 	/**
290 | 	 * Detect test command
291 | 	 */
292 | 	async detectTestCommand() {
293 | 		return this.preflightChecker.detectTestCommand();
294 | 	}
295 | 
296 | 	/**
297 | 	 * Check git working tree
298 | 	 */
299 | 	async checkGitWorkingTree() {
300 | 		return this.preflightChecker.checkGitWorkingTree();
301 | 	}
302 | 
303 | 	/**
304 | 	 * Validate required tools
305 | 	 */
306 | 	async validateRequiredTools() {
307 | 		return this.preflightChecker.validateRequiredTools();
308 | 	}
309 | 
310 | 	/**
311 | 	 * Detect default git branch
312 | 	 */
313 | 	async detectDefaultBranch() {
314 | 		return this.preflightChecker.detectDefaultBranch();
315 | 	}
316 | 
317 | 	// ========== Tag Management ==========
318 | 
319 | 	/**
320 | 	 * Create a new tag
321 | 	 * For file storage: creates tag locally with optional task copying
322 | 	 * For API storage: throws error (client should redirect to web UI)
323 | 	 */
324 | 	async createTag(name: string, options?: CreateTagOptions) {
325 | 		return this.tagService.createTag(name, options);
326 | 	}
327 | 
328 | 	/**
329 | 	 * Delete an existing tag
330 | 	 * Cannot delete master tag
331 | 	 * For file storage: deletes tag locally
332 | 	 * For API storage: throws error (client should redirect to web UI)
333 | 	 */
334 | 	async deleteTag(name: string, options?: DeleteTagOptions) {
335 | 		return this.tagService.deleteTag(name, options);
336 | 	}
337 | 
338 | 	/**
339 | 	 * Rename an existing tag
340 | 	 * Cannot rename master tag
341 | 	 * For file storage: renames tag locally
342 | 	 * For API storage: throws error (client should redirect to web UI)
343 | 	 */
344 | 	async renameTag(oldName: string, newName: string) {
345 | 		return this.tagService.renameTag(oldName, newName);
346 | 	}
347 | 
348 | 	/**
349 | 	 * Copy an existing tag to create a new tag with the same tasks
350 | 	 * For file storage: copies tag locally
351 | 	 * For API storage: throws error (client should show alternative)
352 | 	 */
353 | 	async copyTag(source: string, target: string, options?: CopyTagOptions) {
354 | 		return this.tagService.copyTag(source, target, options);
355 | 	}
356 | 
357 | 	/**
358 | 	 * Get all tags with detailed statistics including task counts
359 | 	 * For API storage, returns briefs with task counts
360 | 	 * For file storage, returns tags from tasks.json with counts
361 | 	 */
362 | 	async getTagsWithStats() {
363 | 		return this.tagService.getTagsWithStats();
364 | 	}
365 | 
366 | 	// ========== Storage Information ==========
367 | 
368 | 	/**
369 | 	 * Get the resolved storage type (actual type being used at runtime)
370 | 	 */
371 | 	getStorageType(): 'file' | 'api' {
372 | 		return this.taskService.getStorageType();
373 | 	}
374 | }
375 | 
```

--------------------------------------------------------------------------------
/apps/extension/src/components/TaskDetails/AIActionsSection.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | import type React from 'react';
  2 | import { useState } from 'react';
  3 | import { Button } from '@/components/ui/button';
  4 | import { Label } from '@/components/ui/label';
  5 | import { Textarea } from '@/components/ui/textarea';
  6 | import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
  7 | import {
  8 | 	Wand2,
  9 | 	Loader2,
 10 | 	PlusCircle,
 11 | 	TrendingUp,
 12 | 	TrendingDown
 13 | } from 'lucide-react';
 14 | import {
 15 | 	useUpdateTask,
 16 | 	useUpdateSubtask,
 17 | 	useScopeUpTask,
 18 | 	useScopeDownTask
 19 | } from '../../webview/hooks/useTaskQueries';
 20 | import type { TaskMasterTask } from '../../webview/types';
 21 | 
 22 | interface AIActionsSectionProps {
 23 | 	currentTask: TaskMasterTask;
 24 | 	isSubtask: boolean;
 25 | 	parentTask?: TaskMasterTask | null;
 26 | 	sendMessage: (message: any) => Promise<any>;
 27 | 	refreshComplexityAfterAI: () => void;
 28 | 	onRegeneratingChange?: (isRegenerating: boolean) => void;
 29 | 	onAppendingChange?: (isAppending: boolean) => void;
 30 | }
 31 | 
 32 | export const AIActionsSection: React.FC<AIActionsSectionProps> = ({
 33 | 	currentTask,
 34 | 	isSubtask,
 35 | 	parentTask,
 36 | 	sendMessage,
 37 | 	refreshComplexityAfterAI,
 38 | 	onRegeneratingChange,
 39 | 	onAppendingChange
 40 | }) => {
 41 | 	const [prompt, setPrompt] = useState('');
 42 | 	const [scopePrompt, setScopePrompt] = useState('');
 43 | 	const [scopeStrength, setScopeStrength] = useState<
 44 | 		'light' | 'regular' | 'heavy'
 45 | 	>('regular');
 46 | 	const [lastAction, setLastAction] = useState<
 47 | 		'regenerate' | 'append' | 'scope-up' | 'scope-down' | null
 48 | 	>(null);
 49 | 	const updateTask = useUpdateTask();
 50 | 	const updateSubtask = useUpdateSubtask();
 51 | 	const scopeUpTask = useScopeUpTask();
 52 | 	const scopeDownTask = useScopeDownTask();
 53 | 
 54 | 	const handleRegenerate = async () => {
 55 | 		if (!currentTask || !prompt.trim()) {
 56 | 			return;
 57 | 		}
 58 | 
 59 | 		setLastAction('regenerate');
 60 | 		onRegeneratingChange?.(true);
 61 | 
 62 | 		try {
 63 | 			if (isSubtask && parentTask) {
 64 | 				await updateSubtask.mutateAsync({
 65 | 					taskId: `${parentTask.id}.${currentTask.id}`,
 66 | 					prompt: prompt,
 67 | 					options: { research: false }
 68 | 				});
 69 | 			} else {
 70 | 				await updateTask.mutateAsync({
 71 | 					taskId: currentTask.id,
 72 | 					updates: { description: prompt },
 73 | 					options: { append: false, research: false }
 74 | 				});
 75 | 			}
 76 | 
 77 | 			setPrompt('');
 78 | 			refreshComplexityAfterAI();
 79 | 		} catch (error) {
 80 | 			console.error('❌ TaskDetailsView: Failed to regenerate task:', error);
 81 | 		} finally {
 82 | 			setLastAction(null);
 83 | 			onRegeneratingChange?.(false);
 84 | 		}
 85 | 	};
 86 | 
 87 | 	const handleAppend = async () => {
 88 | 		if (!currentTask || !prompt.trim()) {
 89 | 			return;
 90 | 		}
 91 | 
 92 | 		setLastAction('append');
 93 | 		onAppendingChange?.(true);
 94 | 
 95 | 		try {
 96 | 			if (isSubtask && parentTask) {
 97 | 				await updateSubtask.mutateAsync({
 98 | 					taskId: `${parentTask.id}.${currentTask.id}`,
 99 | 					prompt: prompt,
100 | 					options: { research: false }
101 | 				});
102 | 			} else {
103 | 				await updateTask.mutateAsync({
104 | 					taskId: currentTask.id,
105 | 					updates: { description: prompt },
106 | 					options: { append: true, research: false }
107 | 				});
108 | 			}
109 | 
110 | 			setPrompt('');
111 | 			refreshComplexityAfterAI();
112 | 		} catch (error) {
113 | 			console.error('❌ TaskDetailsView: Failed to append to task:', error);
114 | 		} finally {
115 | 			setLastAction(null);
116 | 			onAppendingChange?.(false);
117 | 		}
118 | 	};
119 | 
120 | 	const handleScopeUp = async () => {
121 | 		if (!currentTask) {
122 | 			return;
123 | 		}
124 | 
125 | 		setLastAction('scope-up');
126 | 
127 | 		try {
128 | 			const taskId =
129 | 				isSubtask && parentTask
130 | 					? `${parentTask.id}.${currentTask.id}`
131 | 					: currentTask.id;
132 | 
133 | 			await scopeUpTask.mutateAsync({
134 | 				taskId,
135 | 				strength: scopeStrength,
136 | 				prompt: scopePrompt.trim() || undefined,
137 | 				options: { research: false }
138 | 			});
139 | 
140 | 			setScopePrompt('');
141 | 			refreshComplexityAfterAI();
142 | 		} catch (error) {
143 | 			console.error('❌ AIActionsSection: Failed to scope up task:', error);
144 | 		} finally {
145 | 			setLastAction(null);
146 | 		}
147 | 	};
148 | 
149 | 	const handleScopeDown = async () => {
150 | 		if (!currentTask) {
151 | 			return;
152 | 		}
153 | 
154 | 		setLastAction('scope-down');
155 | 
156 | 		try {
157 | 			const taskId =
158 | 				isSubtask && parentTask
159 | 					? `${parentTask.id}.${currentTask.id}`
160 | 					: currentTask.id;
161 | 
162 | 			await scopeDownTask.mutateAsync({
163 | 				taskId,
164 | 				strength: scopeStrength,
165 | 				prompt: scopePrompt.trim() || undefined,
166 | 				options: { research: false }
167 | 			});
168 | 
169 | 			setScopePrompt('');
170 | 			refreshComplexityAfterAI();
171 | 		} catch (error) {
172 | 			console.error('❌ AIActionsSection: Failed to scope down task:', error);
173 | 		} finally {
174 | 			setLastAction(null);
175 | 		}
176 | 	};
177 | 
178 | 	// Track loading states based on the last action
179 | 	const isLoading =
180 | 		updateTask.isPending ||
181 | 		updateSubtask.isPending ||
182 | 		scopeUpTask.isPending ||
183 | 		scopeDownTask.isPending;
184 | 	const isRegenerating = isLoading && lastAction === 'regenerate';
185 | 	const isAppending = isLoading && lastAction === 'append';
186 | 	const isScopingUp = isLoading && lastAction === 'scope-up';
187 | 	const isScopingDown = isLoading && lastAction === 'scope-down';
188 | 
189 | 	return (
190 | 		<CollapsibleSection
191 | 			title="AI Actions"
192 | 			icon={Wand2}
193 | 			defaultExpanded={true}
194 | 			buttonClassName="text-vscode-foreground/80 hover:text-vscode-foreground"
195 | 		>
196 | 			<div className="space-y-6">
197 | 				{/* Standard AI Actions Section */}
198 | 				<div className="space-y-4">
199 | 					<div>
200 | 						<Label
201 | 							htmlFor="ai-prompt"
202 | 							className="block text-sm font-medium text-vscode-foreground/80 mb-2"
203 | 						>
204 | 							Enter your prompt
205 | 						</Label>
206 | 						<Textarea
207 | 							id="ai-prompt"
208 | 							placeholder={
209 | 								isSubtask
210 | 									? 'Describe implementation notes, progress updates, or findings to add to this subtask...'
211 | 									: 'Describe what you want to change or add to this task...'
212 | 							}
213 | 							value={prompt}
214 | 							onChange={(e) => setPrompt(e.target.value)}
215 | 							className="min-h-[100px] bg-vscode-input-background border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
216 | 							disabled={isLoading}
217 | 						/>
218 | 					</div>
219 | 
220 | 					<div className="flex gap-3">
221 | 						{!isSubtask && (
222 | 							<Button
223 | 								onClick={handleRegenerate}
224 | 								disabled={!prompt.trim() || isLoading}
225 | 								className="bg-primary text-primary-foreground hover:bg-primary/90"
226 | 							>
227 | 								{isRegenerating ? (
228 | 									<>
229 | 										<Loader2 className="w-4 h-4 mr-2 animate-spin" />
230 | 										Regenerating...
231 | 									</>
232 | 								) : (
233 | 									<>
234 | 										<Wand2 className="w-4 h-4 mr-2" />
235 | 										Regenerate Task
236 | 									</>
237 | 								)}
238 | 							</Button>
239 | 						)}
240 | 
241 | 						<Button
242 | 							onClick={handleAppend}
243 | 							disabled={!prompt.trim() || isLoading}
244 | 							variant={isSubtask ? 'default' : 'outline'}
245 | 							className={
246 | 								isSubtask
247 | 									? 'bg-primary text-primary-foreground hover:bg-primary/90'
248 | 									: 'bg-secondary text-secondary-foreground hover:bg-secondary/90 border-widget-border'
249 | 							}
250 | 						>
251 | 							{isAppending ? (
252 | 								<>
253 | 									<Loader2 className="w-4 h-4 mr-2 animate-spin" />
254 | 									{isSubtask ? 'Updating...' : 'Appending...'}
255 | 								</>
256 | 							) : (
257 | 								<>
258 | 									<PlusCircle className="w-4 h-4 mr-2" />
259 | 									{isSubtask ? 'Add Notes to Subtask' : 'Append to Task'}
260 | 								</>
261 | 							)}
262 | 						</Button>
263 | 					</div>
264 | 				</div>
265 | 
266 | 				{/* Scope Adjustment Section */}
267 | 				<div className="border-t border-vscode-widget-border pt-4 space-y-4">
268 | 					<div>
269 | 						<Label className="block text-sm font-medium text-vscode-foreground/80 mb-3">
270 | 							Task Complexity Adjustment
271 | 						</Label>
272 | 
273 | 						{/* Strength Selection */}
274 | 						<div className="mb-3">
275 | 							<Label className="block text-xs text-vscode-foreground/60 mb-2">
276 | 								Adjustment Strength
277 | 							</Label>
278 | 							<div className="flex gap-2">
279 | 								{(['light', 'regular', 'heavy'] as const).map((strength) => (
280 | 									<Button
281 | 										key={strength}
282 | 										onClick={() => setScopeStrength(strength)}
283 | 										variant={scopeStrength === strength ? 'default' : 'outline'}
284 | 										size="sm"
285 | 										className={
286 | 											scopeStrength === strength
287 | 												? 'bg-accent text-accent-foreground border-accent'
288 | 												: 'border-widget-border text-vscode-foreground/80 hover:bg-vscode-list-hoverBackground'
289 | 										}
290 | 										disabled={isLoading}
291 | 									>
292 | 										{strength.charAt(0).toUpperCase() + strength.slice(1)}
293 | 									</Button>
294 | 								))}
295 | 							</div>
296 | 						</div>
297 | 
298 | 						{/* Scope Prompt */}
299 | 						<Textarea
300 | 							placeholder="Optional: Specify how to adjust complexity (e.g., 'Focus on error handling', 'Remove unnecessary details', 'Add more implementation steps')"
301 | 							value={scopePrompt}
302 | 							onChange={(e) => setScopePrompt(e.target.value)}
303 | 							className="min-h-[80px] bg-vscode-input-background border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
304 | 							disabled={isLoading}
305 | 						/>
306 | 					</div>
307 | 
308 | 					<div className="flex gap-3">
309 | 						<Button
310 | 							onClick={handleScopeUp}
311 | 							disabled={isLoading}
312 | 							variant="outline"
313 | 							className="flex-1 border-green-600/50 text-green-400 hover:bg-green-600/10 hover:border-green-500"
314 | 						>
315 | 							{isScopingUp ? (
316 | 								<>
317 | 									<Loader2 className="w-4 h-4 mr-2 animate-spin" />
318 | 									Scoping Up...
319 | 								</>
320 | 							) : (
321 | 								<>
322 | 									<TrendingUp className="w-4 h-4 mr-2" />
323 | 									Scope Up
324 | 								</>
325 | 							)}
326 | 						</Button>
327 | 
328 | 						<Button
329 | 							onClick={handleScopeDown}
330 | 							disabled={isLoading}
331 | 							variant="outline"
332 | 							className="flex-1 border-blue-600/50 text-blue-400 hover:bg-blue-600/10 hover:border-blue-500"
333 | 						>
334 | 							{isScopingDown ? (
335 | 								<>
336 | 									<Loader2 className="w-4 h-4 mr-2 animate-spin" />
337 | 									Scoping Down...
338 | 								</>
339 | 							) : (
340 | 								<>
341 | 									<TrendingDown className="w-4 h-4 mr-2" />
342 | 									Scope Down
343 | 								</>
344 | 							)}
345 | 						</Button>
346 | 					</div>
347 | 				</div>
348 | 
349 | 				{/* Help Text */}
350 | 				<div className="text-xs text-vscode-foreground/60 space-y-1">
351 | 					{isSubtask ? (
352 | 						<p>
353 | 							<strong>Add Notes:</strong> Appends timestamped implementation
354 | 							notes, progress updates, or findings to this subtask's details
355 | 						</p>
356 | 					) : (
357 | 						<>
358 | 							<p>
359 | 								<strong>Regenerate:</strong> Completely rewrites the task
360 | 								description and subtasks based on your prompt
361 | 							</p>
362 | 							<p>
363 | 								<strong>Append:</strong> Adds new content to the existing task
364 | 								implementation details based on your prompt
365 | 							</p>
366 | 						</>
367 | 					)}
368 | 					<p>
369 | 						<strong>Scope Up:</strong> Increases task complexity with more
370 | 						details, requirements, or implementation steps
371 | 					</p>
372 | 					<p>
373 | 						<strong>Scope Down:</strong> Decreases task complexity by
374 | 						simplifying or removing unnecessary details
375 | 					</p>
376 | 				</div>
377 | 			</div>
378 | 		</CollapsibleSection>
379 | 	);
380 | };
381 | 
```

--------------------------------------------------------------------------------
/.taskmaster/docs/tdd-workflow-phase-2-pr-resumability.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Phase 2: PR + Resumability - Autonomous TDD Workflow
  2 | 
  3 | ## Objective
  4 | Add PR creation with GitHub CLI integration, resumable checkpoints for interrupted runs, and enhanced guardrails with coverage enforcement.
  5 | 
  6 | ## Scope
  7 | - GitHub PR creation via `gh` CLI
  8 | - Well-formed PR body using run report
  9 | - Resumable checkpoints and `--resume` flag
 10 | - Coverage enforcement before finalization
 11 | - Optional lint/format step
 12 | - Enhanced error recovery
 13 | 
 14 | ## Deliverables
 15 | 
 16 | ### 1. PR Creation Integration
 17 | 
 18 | **PRAdapter** (`packages/tm-core/src/services/pr-adapter.ts`):
 19 | ```typescript
 20 | class PRAdapter {
 21 |   async isGHAvailable(): Promise<boolean>
 22 |   async createPR(options: PROptions): Promise<PRResult>
 23 |   async getPRTemplate(runReport: RunReport): Promise<string>
 24 | 
 25 |   // Fallback for missing gh CLI
 26 |   async getManualPRInstructions(options: PROptions): Promise<string>
 27 | }
 28 | 
 29 | interface PROptions {
 30 |   branch: string
 31 |   base: string
 32 |   title: string
 33 |   body: string
 34 |   draft?: boolean
 35 | }
 36 | 
 37 | interface PRResult {
 38 |   url: string
 39 |   number: number
 40 | }
 41 | ```
 42 | 
 43 | **PR Title Format:**
 44 | ```
 45 | Task #<id> [<tag>]: <title>
 46 | ```
 47 | 
 48 | Example: `Task #42 [analytics]: User metrics tracking`
 49 | 
 50 | **PR Body Template:**
 51 | 
 52 | Located at `.taskmaster/templates/pr-body.md`:
 53 | 
 54 | ```markdown
 55 | ## Summary
 56 | 
 57 | Implements Task #42 from TaskMaster autonomous workflow.
 58 | 
 59 | **Branch:** {branch}
 60 | **Tag:** {tag}
 61 | **Subtasks completed:** {subtaskCount}
 62 | 
 63 | {taskDescription}
 64 | 
 65 | ## Subtasks
 66 | 
 67 | {subtasksList}
 68 | 
 69 | ## Test Coverage
 70 | 
 71 | | Metric | Coverage |
 72 | |--------|----------|
 73 | | Lines | {lines}% |
 74 | | Branches | {branches}% |
 75 | | Functions | {functions}% |
 76 | | Statements | {statements}% |
 77 | 
 78 | **All subtasks passed with {totalTests} tests.**
 79 | 
 80 | ## Commits
 81 | 
 82 | {commitsList}
 83 | 
 84 | ## Run Report
 85 | 
 86 | Full execution report: `.taskmaster/reports/runs/{runId}/`
 87 | 
 88 | ---
 89 | 
 90 | 🤖 Generated with [Task Master](https://github.com/cline/task-master) autonomous TDD workflow
 91 | ```
 92 | 
 93 | **Token replacement:**
 94 | - `{branch}` → branch name
 95 | - `{tag}` → active tag
 96 | - `{subtaskCount}` → number of completed subtasks
 97 | - `{taskDescription}` → task description from TaskMaster
 98 | - `{subtasksList}` → markdown list of subtask titles
 99 | - `{lines}`, `{branches}`, `{functions}`, `{statements}` → coverage percentages
100 | - `{totalTests}` → total test count
101 | - `{commitsList}` → markdown list of commit SHAs and messages
102 | - `{runId}` → run ID timestamp
103 | 
104 | ### 2. GitHub CLI Integration
105 | 
106 | **Detection:**
107 | ```bash
108 | which gh
109 | ```
110 | 
111 | If not found, show fallback instructions:
112 | ```bash
113 | ✓ Branch pushed: analytics/task-42-user-metrics
114 | ✗ gh CLI not found - cannot create PR automatically
115 | 
116 | To create PR manually:
117 |   gh pr create \
118 |     --base main \
119 |     --head analytics/task-42-user-metrics \
120 |     --title "Task #42 [analytics]: User metrics tracking" \
121 |     --body-file .taskmaster/reports/runs/2025-01-15-142033/pr.md
122 | 
123 | Or visit:
124 |   https://github.com/org/repo/compare/main...analytics/task-42-user-metrics
125 | ```
126 | 
127 | **Confirmation gate:**
128 | ```bash
129 | Ready to create PR:
130 |   Title: Task #42 [analytics]: User metrics tracking
131 |   Base: main
132 |   Head: analytics/task-42-user-metrics
133 | 
134 | Create PR? [Y/n]
135 | ```
136 | 
137 | Unless `--no-confirm` flag is set.
138 | 
139 | ### 3. Resumable Workflow
140 | 
141 | **State Checkpoint** (`state.json`):
142 | ```json
143 | {
144 |   "runId": "2025-01-15-142033",
145 |   "taskId": "42",
146 |   "phase": "subtask-loop",
147 |   "currentSubtask": "42.2",
148 |   "currentPhase": "green",
149 |   "attempts": 2,
150 |   "completedSubtasks": ["42.1"],
151 |   "commits": ["a1b2c3d"],
152 |   "branch": "analytics/task-42-user-metrics",
153 |   "tag": "analytics",
154 |   "canResume": true,
155 |   "pausedAt": "2025-01-15T14:25:35Z",
156 |   "pausedReason": "max_attempts_reached",
157 |   "nextAction": "manual_review_required"
158 | }
159 | ```
160 | 
161 | **Resume Command:**
162 | ```bash
163 | $ tm autopilot --resume
164 | 
165 | Resuming run: 2025-01-15-142033
166 |   Task: #42 [analytics] User metrics tracking
167 |   Branch: analytics/task-42-user-metrics
168 |   Last subtask: 42.2 (GREEN phase, attempt 2/3 failed)
169 |   Paused: 5 minutes ago
170 | 
171 | Reason: Could not achieve green state after 3 attempts
172 | Last error: POST /api/metrics returns 500 instead of 201
173 | 
174 | Resume from subtask 42.2 GREEN phase? [Y/n]
175 | ```
176 | 
177 | **Resume logic:**
178 | 1. Load state from `.taskmaster/reports/runs/<runId>/state.json`
179 | 2. Verify branch still exists and is checked out
180 | 3. Verify no uncommitted changes (unless `--force`)
181 | 4. Continue from last checkpoint phase
182 | 5. Update state file as execution progresses
183 | 
184 | **Multiple interrupted runs:**
185 | ```bash
186 | $ tm autopilot --resume
187 | 
188 | Found 2 resumable runs:
189 |   1. 2025-01-15-142033 - Task #42 (paused 5 min ago at subtask 42.2 GREEN)
190 |   2. 2025-01-14-103022 - Task #38 (paused 2 hours ago at subtask 38.3 RED)
191 | 
192 | Select run to resume [1-2]:
193 | ```
194 | 
195 | ### 4. Coverage Enforcement
196 | 
197 | **Coverage Check Phase** (before finalization):
198 | ```typescript
199 | async function enforceCoverage(runId: string): Promise<void> {
200 |   const testResults = await testRunner.runAll()
201 |   const coverage = await testRunner.getCoverage()
202 | 
203 |   const thresholds = config.test.coverageThresholds
204 |   const failures = []
205 | 
206 |   if (coverage.lines < thresholds.lines) {
207 |     failures.push(`Lines: ${coverage.lines}% < ${thresholds.lines}%`)
208 |   }
209 |   // ... check branches, functions, statements
210 | 
211 |   if (failures.length > 0) {
212 |     throw new CoverageError(
213 |       `Coverage thresholds not met:\n${failures.join('\n')}`
214 |     )
215 |   }
216 | 
217 |   // Store coverage in run report
218 |   await storeRunArtifact(runId, 'coverage.json', coverage)
219 | }
220 | ```
221 | 
222 | **Handling coverage failures:**
223 | ```bash
224 | ⚠️  Coverage check failed:
225 |   Lines: 78.5% < 80%
226 |   Branches: 75.0% < 80%
227 | 
228 | Options:
229 |   1. Add more tests and resume
230 |   2. Lower thresholds in .taskmaster/config.json
231 |   3. Skip coverage check: tm autopilot --resume --skip-coverage
232 | 
233 | Run paused. Fix coverage and resume with:
234 |   tm autopilot --resume
235 | ```
236 | 
237 | ### 5. Optional Lint/Format Step
238 | 
239 | **Configuration:**
240 | ```json
241 | {
242 |   "autopilot": {
243 |     "finalization": {
244 |       "lint": {
245 |         "enabled": true,
246 |         "command": "npm run lint",
247 |         "fix": true,
248 |         "failOnError": false
249 |       },
250 |       "format": {
251 |         "enabled": true,
252 |         "command": "npm run format",
253 |         "commitChanges": true
254 |       }
255 |     }
256 |   }
257 | }
258 | ```
259 | 
260 | **Execution:**
261 | ```bash
262 | Finalization Steps:
263 | 
264 |   ✓ All tests passing (12 tests, 0 failures)
265 |   ✓ Coverage thresholds met (85% lines, 82% branches)
266 | 
267 |   LINT Running linter... ⏳
268 |   LINT ✓ No lint errors
269 | 
270 |   FORMAT Running formatter... ⏳
271 |   FORMAT ✓ Formatted 3 files
272 |   FORMAT ✓ Committed formatting changes: "chore: auto-format code"
273 | 
274 |   PUSH Pushing to origin... ⏳
275 |   PUSH ✓ Pushed analytics/task-42-user-metrics
276 | 
277 |   PR Creating pull request... ⏳
278 |   PR ✓ Created PR #123
279 |       https://github.com/org/repo/pull/123
280 | ```
281 | 
282 | ### 6. Enhanced Error Recovery
283 | 
284 | **Pause Points:**
285 | - Max GREEN attempts reached (current)
286 | - Coverage check failed (new)
287 | - Lint errors (if `failOnError: true`)
288 | - Git push failed (new)
289 | - PR creation failed (new)
290 | 
291 | **Each pause saves:**
292 | - Full state checkpoint
293 | - Last command output
294 | - Suggested next actions
295 | - Resume instructions
296 | 
297 | **Automatic recovery attempts:**
298 | - Git push: retry up to 3 times with backoff
299 | - PR creation: fall back to manual instructions
300 | - Lint: auto-fix if enabled, otherwise pause
301 | 
302 | ### 7. Finalization Phase Enhancement
303 | 
304 | **Updated workflow:**
305 | 1. Run full test suite
306 | 2. Check coverage thresholds → pause if failed
307 | 3. Run lint (if enabled) → pause if failed and `failOnError: true`
308 | 4. Run format (if enabled) → auto-commit changes
309 | 5. Confirm push (unless `--no-confirm`)
310 | 6. Push branch → retry on failure
311 | 7. Generate PR body from template
312 | 8. Create PR via gh → fall back to manual instructions
313 | 9. Update task status to 'review' (configurable)
314 | 10. Save final run report
315 | 
316 | **Final output:**
317 | ```bash
318 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
319 | 
320 | ✅ Task #42 [analytics]: User metrics tracking - COMPLETE
321 | 
322 |   Branch: analytics/task-42-user-metrics
323 |   Subtasks completed: 3/3
324 |   Commits: 3
325 |   Total tests: 12 (12 passed, 0 failed)
326 |   Coverage: 85% lines, 82% branches, 88% functions, 85% statements
327 | 
328 |   PR #123: https://github.com/org/repo/pull/123
329 | 
330 |   Run report: .taskmaster/reports/runs/2025-01-15-142033/
331 | 
332 | Next steps:
333 |   - Review PR and request changes if needed
334 |   - Merge when ready
335 |   - Task status updated to 'review'
336 | 
337 | Completed in 24 minutes
338 | ```
339 | 
340 | ## CLI Updates
341 | 
342 | **New flags:**
343 | - `--resume` → Resume from last checkpoint
344 | - `--skip-coverage` → Skip coverage checks
345 | - `--skip-lint` → Skip lint step
346 | - `--skip-format` → Skip format step
347 | - `--skip-pr` → Push branch but don't create PR
348 | - `--draft-pr` → Create draft PR instead of ready-for-review
349 | 
350 | ## Configuration Updates
351 | 
352 | **Add to `.taskmaster/config.json`:**
353 | ```json
354 | {
355 |   "autopilot": {
356 |     "finalization": {
357 |       "lint": {
358 |         "enabled": false,
359 |         "command": "npm run lint",
360 |         "fix": true,
361 |         "failOnError": false
362 |       },
363 |       "format": {
364 |         "enabled": false,
365 |         "command": "npm run format",
366 |         "commitChanges": true
367 |       },
368 |       "updateTaskStatus": "review"
369 |     }
370 |   },
371 |   "git": {
372 |     "pr": {
373 |       "enabled": true,
374 |       "base": "default",
375 |       "bodyTemplate": ".taskmaster/templates/pr-body.md",
376 |       "draft": false
377 |     },
378 |     "pushRetries": 3,
379 |     "pushRetryDelay": 5000
380 |   }
381 | }
382 | ```
383 | 
384 | ## Success Criteria
385 | - Can create PR automatically with well-formed body
386 | - Can resume interrupted runs from any checkpoint
387 | - Coverage checks prevent low-quality code from being merged
388 | - Clear error messages and recovery paths for all failure modes
389 | - Run reports include full PR context for review
390 | 
391 | ## Out of Scope (defer to Phase 3)
392 | - Multiple test framework support (pytest, go test)
393 | - Diff preview before commits
394 | - TUI panel implementation
395 | - Extension/IDE integration
396 | 
397 | ## Testing Strategy
398 | - Mock `gh` CLI for PR creation tests
399 | - Test resume from each possible pause point
400 | - Test coverage failure scenarios
401 | - Test lint/format integration with mock commands
402 | - End-to-end test with PR creation on test repo
403 | 
404 | ## Dependencies
405 | - Phase 1 completed (core workflow)
406 | - GitHub CLI (`gh`) installed (optional, fallback provided)
407 | - Test framework supports coverage output
408 | 
409 | ## Estimated Effort
410 | 1-2 weeks
411 | 
412 | ## Risks & Mitigations
413 | - **Risk:** GitHub CLI auth issues
414 |   - **Mitigation:** Clear auth setup docs, fallback to manual instructions
415 | 
416 | - **Risk:** PR body template doesn't match all project needs
417 |   - **Mitigation:** Make template customizable via config path
418 | 
419 | - **Risk:** Resume state gets corrupted
420 |   - **Mitigation:** Validate state on load, provide --force-reset option
421 | 
422 | - **Risk:** Coverage calculation differs between runs
423 |   - **Mitigation:** Store coverage with each test run for comparison
424 | 
425 | ## Validation
426 | Test with:
427 | - Successful PR creation end-to-end
428 | - Resume from GREEN attempt failure
429 | - Resume from coverage failure
430 | - Resume from lint failure
431 | - Missing `gh` CLI (fallback to manual)
432 | - Lint/format integration enabled
433 | - Multiple interrupted runs (selection UI)
434 | 
```

--------------------------------------------------------------------------------
/scripts/modules/utils/git-utils.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * git-utils.js
  3 |  * Git integration utilities for Task Master
  4 |  * Uses raw git commands and gh CLI for operations
  5 |  * MCP-friendly: All functions require projectRoot parameter
  6 |  */
  7 | 
  8 | import { exec, execSync } from 'child_process';
  9 | import { promisify } from 'util';
 10 | import path from 'path';
 11 | import fs from 'fs';
 12 | 
 13 | const execAsync = promisify(exec);
 14 | 
 15 | /**
 16 |  * Check if the specified directory is inside a git repository
 17 |  * @param {string} projectRoot - Directory to check (required)
 18 |  * @returns {Promise<boolean>} True if inside a git repository
 19 |  */
 20 | async function isGitRepository(projectRoot) {
 21 | 	if (!projectRoot) {
 22 | 		throw new Error('projectRoot is required for isGitRepository');
 23 | 	}
 24 | 
 25 | 	try {
 26 | 		await execAsync('git rev-parse --git-dir', { cwd: projectRoot });
 27 | 		return true;
 28 | 	} catch (error) {
 29 | 		return false;
 30 | 	}
 31 | }
 32 | 
 33 | /**
 34 |  * Get the current git branch name
 35 |  * @param {string} projectRoot - Directory to check (required)
 36 |  * @returns {Promise<string|null>} Current branch name or null if not in git repo
 37 |  */
 38 | async function getCurrentBranch(projectRoot) {
 39 | 	if (!projectRoot) {
 40 | 		throw new Error('projectRoot is required for getCurrentBranch');
 41 | 	}
 42 | 
 43 | 	try {
 44 | 		const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
 45 | 			cwd: projectRoot
 46 | 		});
 47 | 		return stdout.trim();
 48 | 	} catch (error) {
 49 | 		return null;
 50 | 	}
 51 | }
 52 | 
 53 | /**
 54 |  * Get list of all local git branches
 55 |  * @param {string} projectRoot - Directory to check (required)
 56 |  * @returns {Promise<string[]>} Array of branch names
 57 |  */
 58 | async function getLocalBranches(projectRoot) {
 59 | 	if (!projectRoot) {
 60 | 		throw new Error('projectRoot is required for getLocalBranches');
 61 | 	}
 62 | 
 63 | 	try {
 64 | 		const { stdout } = await execAsync(
 65 | 			'git branch --format="%(refname:short)"',
 66 | 			{ cwd: projectRoot }
 67 | 		);
 68 | 		return stdout
 69 | 			.trim()
 70 | 			.split('\n')
 71 | 			.filter((branch) => branch.length > 0)
 72 | 			.map((branch) => branch.trim());
 73 | 	} catch (error) {
 74 | 		return [];
 75 | 	}
 76 | }
 77 | 
 78 | /**
 79 |  * Get list of all remote branches
 80 |  * @param {string} projectRoot - Directory to check (required)
 81 |  * @returns {Promise<string[]>} Array of remote branch names (without remote prefix)
 82 |  */
 83 | async function getRemoteBranches(projectRoot) {
 84 | 	if (!projectRoot) {
 85 | 		throw new Error('projectRoot is required for getRemoteBranches');
 86 | 	}
 87 | 
 88 | 	try {
 89 | 		const { stdout } = await execAsync(
 90 | 			'git branch -r --format="%(refname:short)"',
 91 | 			{ cwd: projectRoot }
 92 | 		);
 93 | 		return stdout
 94 | 			.trim()
 95 | 			.split('\n')
 96 | 			.filter((branch) => branch.length > 0 && !branch.includes('HEAD'))
 97 | 			.map((branch) => branch.replace(/^origin\//, '').trim());
 98 | 	} catch (error) {
 99 | 		return [];
100 | 	}
101 | }
102 | 
103 | /**
104 |  * Check if gh CLI is available and authenticated
105 |  * @param {string} [projectRoot] - Directory context (optional for this check)
106 |  * @returns {Promise<boolean>} True if gh CLI is available and authenticated
107 |  */
108 | async function isGhCliAvailable(projectRoot = null) {
109 | 	try {
110 | 		const options = projectRoot ? { cwd: projectRoot } : {};
111 | 		await execAsync('gh auth status', options);
112 | 		return true;
113 | 	} catch (error) {
114 | 		return false;
115 | 	}
116 | }
117 | 
118 | /**
119 |  * Get GitHub repository information using gh CLI
120 |  * @param {string} projectRoot - Directory to check (required)
121 |  * @returns {Promise<Object|null>} Repository info or null if not available
122 |  */
123 | async function getGitHubRepoInfo(projectRoot) {
124 | 	if (!projectRoot) {
125 | 		throw new Error('projectRoot is required for getGitHubRepoInfo');
126 | 	}
127 | 
128 | 	try {
129 | 		const { stdout } = await execAsync(
130 | 			'gh repo view --json name,owner,defaultBranchRef',
131 | 			{ cwd: projectRoot }
132 | 		);
133 | 		return JSON.parse(stdout);
134 | 	} catch (error) {
135 | 		return null;
136 | 	}
137 | }
138 | 
139 | /**
140 |  * Sanitize branch name to be a valid tag name
141 |  * @param {string} branchName - Git branch name
142 |  * @returns {string} Sanitized tag name
143 |  */
144 | function sanitizeBranchNameForTag(branchName) {
145 | 	if (!branchName || typeof branchName !== 'string') {
146 | 		return 'unknown-branch';
147 | 	}
148 | 
149 | 	// Replace invalid characters with hyphens and clean up
150 | 	return branchName
151 | 		.replace(/[^a-zA-Z0-9_-]/g, '-') // Replace invalid chars with hyphens
152 | 		.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
153 | 		.replace(/-+/g, '-') // Collapse multiple hyphens
154 | 		.toLowerCase() // Convert to lowercase
155 | 		.substring(0, 50); // Limit length
156 | }
157 | 
158 | /**
159 |  * Check if a branch name would create a valid tag name
160 |  * @param {string} branchName - Git branch name
161 |  * @returns {boolean} True if branch name can be converted to valid tag
162 |  */
163 | function isValidBranchForTag(branchName) {
164 | 	if (!branchName || typeof branchName !== 'string') {
165 | 		return false;
166 | 	}
167 | 
168 | 	// Check if it's a reserved branch name that shouldn't become tags
169 | 	const reservedBranches = ['main', 'master', 'develop', 'dev', 'HEAD'];
170 | 	if (reservedBranches.includes(branchName.toLowerCase())) {
171 | 		return false;
172 | 	}
173 | 
174 | 	// Check if sanitized name would be meaningful
175 | 	const sanitized = sanitizeBranchNameForTag(branchName);
176 | 	return sanitized.length > 0 && sanitized !== 'unknown-branch';
177 | }
178 | 
179 | /**
180 |  * Get git repository root directory
181 |  * @param {string} projectRoot - Directory to start search from (required)
182 |  * @returns {Promise<string|null>} Git repository root path or null
183 |  */
184 | async function getGitRepositoryRoot(projectRoot) {
185 | 	if (!projectRoot) {
186 | 		throw new Error('projectRoot is required for getGitRepositoryRoot');
187 | 	}
188 | 
189 | 	try {
190 | 		const { stdout } = await execAsync('git rev-parse --show-toplevel', {
191 | 			cwd: projectRoot
192 | 		});
193 | 		return stdout.trim();
194 | 	} catch (error) {
195 | 		return null;
196 | 	}
197 | }
198 | 
199 | /**
200 |  * Check if specified directory is the git repository root
201 |  * @param {string} projectRoot - Directory to check (required)
202 |  * @returns {Promise<boolean>} True if directory is git root
203 |  */
204 | async function isGitRepositoryRoot(projectRoot) {
205 | 	if (!projectRoot) {
206 | 		throw new Error('projectRoot is required for isGitRepositoryRoot');
207 | 	}
208 | 
209 | 	try {
210 | 		const gitRoot = await getGitRepositoryRoot(projectRoot);
211 | 		return gitRoot && path.resolve(gitRoot) === path.resolve(projectRoot);
212 | 	} catch (error) {
213 | 		return false;
214 | 	}
215 | }
216 | 
217 | /**
218 |  * Get the default branch name for the repository
219 |  * @param {string} projectRoot - Directory to check (required)
220 |  * @returns {Promise<string|null>} Default branch name or null
221 |  */
222 | async function getDefaultBranch(projectRoot) {
223 | 	if (!projectRoot) {
224 | 		throw new Error('projectRoot is required for getDefaultBranch');
225 | 	}
226 | 
227 | 	try {
228 | 		// Try to get from GitHub first (if gh CLI is available)
229 | 		if (await isGhCliAvailable(projectRoot)) {
230 | 			const repoInfo = await getGitHubRepoInfo(projectRoot);
231 | 			if (repoInfo && repoInfo.defaultBranchRef) {
232 | 				return repoInfo.defaultBranchRef.name;
233 | 			}
234 | 		}
235 | 
236 | 		// Fallback to git remote info
237 | 		const { stdout } = await execAsync(
238 | 			'git symbolic-ref refs/remotes/origin/HEAD',
239 | 			{ cwd: projectRoot }
240 | 		);
241 | 		return stdout.replace('refs/remotes/origin/', '').trim();
242 | 	} catch (error) {
243 | 		// Final fallback - common default branch names
244 | 		const commonDefaults = ['main', 'master'];
245 | 		const branches = await getLocalBranches(projectRoot);
246 | 
247 | 		for (const defaultName of commonDefaults) {
248 | 			if (branches.includes(defaultName)) {
249 | 				return defaultName;
250 | 			}
251 | 		}
252 | 
253 | 		return null;
254 | 	}
255 | }
256 | 
257 | /**
258 |  * Check if we're currently on the default branch
259 |  * @param {string} projectRoot - Directory to check (required)
260 |  * @returns {Promise<boolean>} True if on default branch
261 |  */
262 | async function isOnDefaultBranch(projectRoot) {
263 | 	if (!projectRoot) {
264 | 		throw new Error('projectRoot is required for isOnDefaultBranch');
265 | 	}
266 | 
267 | 	try {
268 | 		const currentBranch = await getCurrentBranch(projectRoot);
269 | 		const defaultBranch = await getDefaultBranch(projectRoot);
270 | 		return currentBranch && defaultBranch && currentBranch === defaultBranch;
271 | 	} catch (error) {
272 | 		return false;
273 | 	}
274 | }
275 | 
276 | /**
277 |  * Check and automatically switch tags based on git branch if enabled
278 |  * This runs automatically during task operations, similar to migration
279 |  * @param {string} projectRoot - Project root directory (required)
280 |  * @param {string} tasksPath - Path to tasks.json file
281 |  * @returns {Promise<void>}
282 |  */
283 | async function checkAndAutoSwitchGitTag(projectRoot, tasksPath) {
284 | 	if (!projectRoot) {
285 | 		throw new Error('projectRoot is required for checkAndAutoSwitchGitTag');
286 | 	}
287 | 
288 | 	// DISABLED: Automatic git workflow is too rigid and opinionated
289 | 	// Users should explicitly use git-tag commands if they want integration
290 | 	return;
291 | }
292 | 
293 | /**
294 |  * Synchronous version of git tag checking and switching
295 |  * This runs during readJSON to ensure git integration happens BEFORE tag resolution
296 |  * @param {string} projectRoot - Project root directory (required)
297 |  * @param {string} tasksPath - Path to tasks.json file
298 |  * @returns {void}
299 |  */
300 | function checkAndAutoSwitchGitTagSync(projectRoot, tasksPath) {
301 | 	if (!projectRoot) {
302 | 		return; // Can't proceed without project root
303 | 	}
304 | 
305 | 	// DISABLED: Automatic git workflow is too rigid and opinionated
306 | 	// Users should explicitly use git-tag commands if they want integration
307 | 	return;
308 | }
309 | 
310 | /**
311 |  * Synchronous check if directory is in a git repository
312 |  * @param {string} projectRoot - Directory to check (required)
313 |  * @returns {boolean} True if inside a git repository
314 |  */
315 | function isGitRepositorySync(projectRoot) {
316 | 	if (!projectRoot) {
317 | 		return false;
318 | 	}
319 | 
320 | 	try {
321 | 		execSync('git rev-parse --git-dir', {
322 | 			cwd: projectRoot,
323 | 			stdio: 'ignore' // Suppress output
324 | 		});
325 | 		return true;
326 | 	} catch (error) {
327 | 		return false;
328 | 	}
329 | }
330 | 
331 | /**
332 |  * Synchronous get current git branch name
333 |  * @param {string} projectRoot - Directory to check (required)
334 |  * @returns {string|null} Current branch name or null if not in git repo
335 |  */
336 | function getCurrentBranchSync(projectRoot) {
337 | 	if (!projectRoot) {
338 | 		return null;
339 | 	}
340 | 
341 | 	try {
342 | 		const stdout = execSync('git rev-parse --abbrev-ref HEAD', {
343 | 			cwd: projectRoot,
344 | 			encoding: 'utf8'
345 | 		});
346 | 		return stdout.trim();
347 | 	} catch (error) {
348 | 		return null;
349 | 	}
350 | }
351 | 
352 | /**
353 |  * Check if the current working directory is inside a Git work-tree.
354 |  * Uses `git rev-parse --is-inside-work-tree` which is more specific than --git-dir
355 |  * for detecting work-trees (excludes bare repos and .git directories).
356 |  * This is ideal for preventing accidental git init in existing work-trees.
357 |  * @returns {boolean} True if inside a Git work-tree, false otherwise.
358 |  */
359 | function insideGitWorkTree() {
360 | 	try {
361 | 		execSync('git rev-parse --is-inside-work-tree', {
362 | 			stdio: 'ignore',
363 | 			cwd: process.cwd()
364 | 		});
365 | 		return true;
366 | 	} catch {
367 | 		return false;
368 | 	}
369 | }
370 | 
371 | // Export all functions
372 | export {
373 | 	isGitRepository,
374 | 	getCurrentBranch,
375 | 	getLocalBranches,
376 | 	getRemoteBranches,
377 | 	isGhCliAvailable,
378 | 	getGitHubRepoInfo,
379 | 	sanitizeBranchNameForTag,
380 | 	isValidBranchForTag,
381 | 	getGitRepositoryRoot,
382 | 	isGitRepositoryRoot,
383 | 	getDefaultBranch,
384 | 	isOnDefaultBranch,
385 | 	checkAndAutoSwitchGitTag,
386 | 	checkAndAutoSwitchGitTagSync,
387 | 	isGitRepositorySync,
388 | 	getCurrentBranchSync,
389 | 	insideGitWorkTree
390 | };
391 | 
```

--------------------------------------------------------------------------------
/tests/unit/mcp/tools/get-tasks.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Tests for the get-tasks MCP tool
  3 |  *
  4 |  * This test verifies the MCP tool properly handles comma-separated status filtering
  5 |  * and passes arguments correctly to the underlying direct function.
  6 |  */
  7 | 
  8 | import { jest } from '@jest/globals';
  9 | import {
 10 | 	sampleTasks,
 11 | 	emptySampleTasks
 12 | } from '../../../fixtures/sample-tasks.js';
 13 | 
 14 | // Mock EVERYTHING
 15 | const mockListTasksDirect = jest.fn();
 16 | jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
 17 | 	listTasksDirect: mockListTasksDirect
 18 | }));
 19 | 
 20 | const mockHandleApiResult = jest.fn((result) => result);
 21 | const mockWithNormalizedProjectRoot = jest.fn((executeFn) => executeFn);
 22 | const mockCreateErrorResponse = jest.fn((msg) => ({
 23 | 	success: false,
 24 | 	error: { code: 'ERROR', message: msg }
 25 | }));
 26 | 
 27 | const mockResolveTasksPath = jest.fn(() => '/mock/project/tasks.json');
 28 | const mockResolveComplexityReportPath = jest.fn(
 29 | 	() => '/mock/project/complexity-report.json'
 30 | );
 31 | 
 32 | jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
 33 | 	withNormalizedProjectRoot: mockWithNormalizedProjectRoot,
 34 | 	handleApiResult: mockHandleApiResult,
 35 | 	createErrorResponse: mockCreateErrorResponse,
 36 | 	createContentResponse: jest.fn((content) => ({
 37 | 		success: true,
 38 | 		data: content
 39 | 	}))
 40 | }));
 41 | 
 42 | jest.mock('../../../../mcp-server/src/core/utils/path-utils.js', () => ({
 43 | 	resolveTasksPath: mockResolveTasksPath,
 44 | 	resolveComplexityReportPath: mockResolveComplexityReportPath
 45 | }));
 46 | 
 47 | // Mock the z object from zod
 48 | const mockZod = {
 49 | 	object: jest.fn(() => mockZod),
 50 | 	string: jest.fn(() => mockZod),
 51 | 	boolean: jest.fn(() => mockZod),
 52 | 	optional: jest.fn(() => mockZod),
 53 | 	describe: jest.fn(() => mockZod),
 54 | 	_def: {
 55 | 		shape: () => ({
 56 | 			status: {},
 57 | 			withSubtasks: {},
 58 | 			file: {},
 59 | 			complexityReport: {},
 60 | 			projectRoot: {}
 61 | 		})
 62 | 	}
 63 | };
 64 | 
 65 | jest.mock('zod', () => ({
 66 | 	z: mockZod
 67 | }));
 68 | 
 69 | // DO NOT import the real module - create a fake implementation
 70 | const registerListTasksTool = (server) => {
 71 | 	const toolConfig = {
 72 | 		name: 'get_tasks',
 73 | 		description:
 74 | 			'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
 75 | 		parameters: mockZod,
 76 | 
 77 | 		execute: (args, context) => {
 78 | 			const { log, session } = context;
 79 | 
 80 | 			try {
 81 | 				log.info &&
 82 | 					log.info(`Getting tasks with filters: ${JSON.stringify(args)}`);
 83 | 
 84 | 				// Resolve paths using mock functions
 85 | 				let tasksJsonPath;
 86 | 				try {
 87 | 					tasksJsonPath = mockResolveTasksPath(args, log);
 88 | 				} catch (error) {
 89 | 					log.error && log.error(`Error finding tasks.json: ${error.message}`);
 90 | 					return mockCreateErrorResponse(
 91 | 						`Failed to find tasks.json: ${error.message}`
 92 | 					);
 93 | 				}
 94 | 
 95 | 				let complexityReportPath;
 96 | 				try {
 97 | 					complexityReportPath = mockResolveComplexityReportPath(args, session);
 98 | 				} catch (error) {
 99 | 					log.error &&
100 | 						log.error(`Error finding complexity report: ${error.message}`);
101 | 					complexityReportPath = null;
102 | 				}
103 | 
104 | 				const result = mockListTasksDirect(
105 | 					{
106 | 						tasksJsonPath: tasksJsonPath,
107 | 						status: args.status,
108 | 						withSubtasks: args.withSubtasks,
109 | 						reportPath: complexityReportPath
110 | 					},
111 | 					log
112 | 				);
113 | 
114 | 				log.info &&
115 | 					log.info(
116 | 						`Retrieved ${result.success ? result.data?.tasks?.length || 0 : 0} tasks`
117 | 					);
118 | 				return mockHandleApiResult(result, log, 'Error getting tasks');
119 | 			} catch (error) {
120 | 				log.error && log.error(`Error getting tasks: ${error.message}`);
121 | 				return mockCreateErrorResponse(error.message);
122 | 			}
123 | 		}
124 | 	};
125 | 
126 | 	server.addTool(toolConfig);
127 | };
128 | 
129 | describe('MCP Tool: get-tasks', () => {
130 | 	let mockServer;
131 | 	let executeFunction;
132 | 
133 | 	const mockLogger = {
134 | 		debug: jest.fn(),
135 | 		info: jest.fn(),
136 | 		warn: jest.fn(),
137 | 		error: jest.fn()
138 | 	};
139 | 
140 | 	// Sample response data with different statuses for testing
141 | 	const tasksResponse = {
142 | 		success: true,
143 | 		data: {
144 | 			tasks: [
145 | 				{ id: 1, title: 'Task 1', status: 'done' },
146 | 				{ id: 2, title: 'Task 2', status: 'pending' },
147 | 				{ id: 3, title: 'Task 3', status: 'in-progress' },
148 | 				{ id: 4, title: 'Task 4', status: 'blocked' },
149 | 				{ id: 5, title: 'Task 5', status: 'deferred' },
150 | 				{ id: 6, title: 'Task 6', status: 'review' }
151 | 			],
152 | 			filter: 'all',
153 | 			stats: {
154 | 				total: 6,
155 | 				completed: 1,
156 | 				inProgress: 1,
157 | 				pending: 1,
158 | 				blocked: 1,
159 | 				deferred: 1,
160 | 				review: 1
161 | 			}
162 | 		}
163 | 	};
164 | 
165 | 	beforeEach(() => {
166 | 		jest.clearAllMocks();
167 | 
168 | 		mockServer = {
169 | 			addTool: jest.fn((config) => {
170 | 				executeFunction = config.execute;
171 | 			})
172 | 		};
173 | 
174 | 		// Setup default successful response
175 | 		mockListTasksDirect.mockReturnValue(tasksResponse);
176 | 
177 | 		// Register the tool
178 | 		registerListTasksTool(mockServer);
179 | 	});
180 | 
181 | 	test('should register the tool correctly', () => {
182 | 		expect(mockServer.addTool).toHaveBeenCalledWith(
183 | 			expect.objectContaining({
184 | 				name: 'get_tasks',
185 | 				description: expect.stringContaining('Get all tasks from Task Master'),
186 | 				parameters: expect.any(Object),
187 | 				execute: expect.any(Function)
188 | 			})
189 | 		);
190 | 	});
191 | 
192 | 	test('should handle single status filter', () => {
193 | 		const mockContext = {
194 | 			log: mockLogger,
195 | 			session: { workingDirectory: '/mock/dir' }
196 | 		};
197 | 
198 | 		const args = {
199 | 			status: 'pending',
200 | 			withSubtasks: false,
201 | 			projectRoot: '/mock/project'
202 | 		};
203 | 
204 | 		executeFunction(args, mockContext);
205 | 
206 | 		expect(mockListTasksDirect).toHaveBeenCalledWith(
207 | 			expect.objectContaining({
208 | 				status: 'pending'
209 | 			}),
210 | 			mockLogger
211 | 		);
212 | 	});
213 | 
214 | 	test('should handle comma-separated status filter', () => {
215 | 		const mockContext = {
216 | 			log: mockLogger,
217 | 			session: { workingDirectory: '/mock/dir' }
218 | 		};
219 | 
220 | 		const args = {
221 | 			status: 'done,pending,in-progress',
222 | 			withSubtasks: false,
223 | 			projectRoot: '/mock/project'
224 | 		};
225 | 
226 | 		executeFunction(args, mockContext);
227 | 
228 | 		expect(mockListTasksDirect).toHaveBeenCalledWith(
229 | 			expect.objectContaining({
230 | 				status: 'done,pending,in-progress'
231 | 			}),
232 | 			mockLogger
233 | 		);
234 | 	});
235 | 
236 | 	test('should handle comma-separated status with spaces', () => {
237 | 		const mockContext = {
238 | 			log: mockLogger,
239 | 			session: { workingDirectory: '/mock/dir' }
240 | 		};
241 | 
242 | 		const args = {
243 | 			status: 'blocked, deferred , review',
244 | 			withSubtasks: true,
245 | 			projectRoot: '/mock/project'
246 | 		};
247 | 
248 | 		executeFunction(args, mockContext);
249 | 
250 | 		expect(mockListTasksDirect).toHaveBeenCalledWith(
251 | 			expect.objectContaining({
252 | 				status: 'blocked, deferred , review',
253 | 				withSubtasks: true
254 | 			}),
255 | 			mockLogger
256 | 		);
257 | 	});
258 | 
259 | 	test('should handle withSubtasks parameter correctly', () => {
260 | 		const mockContext = {
261 | 			log: mockLogger,
262 | 			session: { workingDirectory: '/mock/dir' }
263 | 		};
264 | 
265 | 		// Test with withSubtasks=true
266 | 		executeFunction(
267 | 			{
268 | 				status: 'pending',
269 | 				withSubtasks: true,
270 | 				projectRoot: '/mock/project'
271 | 			},
272 | 			mockContext
273 | 		);
274 | 
275 | 		expect(mockListTasksDirect).toHaveBeenCalledWith(
276 | 			expect.objectContaining({
277 | 				withSubtasks: true
278 | 			}),
279 | 			mockLogger
280 | 		);
281 | 
282 | 		jest.clearAllMocks();
283 | 
284 | 		// Test with withSubtasks=false
285 | 		executeFunction(
286 | 			{
287 | 				status: 'pending',
288 | 				withSubtasks: false,
289 | 				projectRoot: '/mock/project'
290 | 			},
291 | 			mockContext
292 | 		);
293 | 
294 | 		expect(mockListTasksDirect).toHaveBeenCalledWith(
295 | 			expect.objectContaining({
296 | 				withSubtasks: false
297 | 			}),
298 | 			mockLogger
299 | 		);
300 | 	});
301 | 
302 | 	test('should handle path resolution errors gracefully', () => {
303 | 		mockResolveTasksPath.mockImplementationOnce(() => {
304 | 			throw new Error('Tasks file not found');
305 | 		});
306 | 
307 | 		const mockContext = {
308 | 			log: mockLogger,
309 | 			session: { workingDirectory: '/mock/dir' }
310 | 		};
311 | 
312 | 		const args = {
313 | 			status: 'pending',
314 | 			projectRoot: '/mock/project'
315 | 		};
316 | 
317 | 		const result = executeFunction(args, mockContext);
318 | 
319 | 		expect(mockLogger.error).toHaveBeenCalledWith(
320 | 			'Error finding tasks.json: Tasks file not found'
321 | 		);
322 | 		expect(mockCreateErrorResponse).toHaveBeenCalledWith(
323 | 			'Failed to find tasks.json: Tasks file not found'
324 | 		);
325 | 	});
326 | 
327 | 	test('should handle complexity report path resolution errors gracefully', () => {
328 | 		mockResolveComplexityReportPath.mockImplementationOnce(() => {
329 | 			throw new Error('Complexity report not found');
330 | 		});
331 | 
332 | 		const mockContext = {
333 | 			log: mockLogger,
334 | 			session: { workingDirectory: '/mock/dir' }
335 | 		};
336 | 
337 | 		const args = {
338 | 			status: 'pending',
339 | 			projectRoot: '/mock/project'
340 | 		};
341 | 
342 | 		executeFunction(args, mockContext);
343 | 
344 | 		// Should not fail the operation but set complexityReportPath to null
345 | 		expect(mockListTasksDirect).toHaveBeenCalledWith(
346 | 			expect.objectContaining({
347 | 				reportPath: null
348 | 			}),
349 | 			mockLogger
350 | 		);
351 | 	});
352 | 
353 | 	test('should handle listTasksDirect errors', () => {
354 | 		const errorResponse = {
355 | 			success: false,
356 | 			error: {
357 | 				code: 'LIST_TASKS_ERROR',
358 | 				message: 'Failed to list tasks'
359 | 			}
360 | 		};
361 | 
362 | 		mockListTasksDirect.mockReturnValueOnce(errorResponse);
363 | 
364 | 		const mockContext = {
365 | 			log: mockLogger,
366 | 			session: { workingDirectory: '/mock/dir' }
367 | 		};
368 | 
369 | 		const args = {
370 | 			status: 'pending',
371 | 			projectRoot: '/mock/project'
372 | 		};
373 | 
374 | 		executeFunction(args, mockContext);
375 | 
376 | 		expect(mockHandleApiResult).toHaveBeenCalledWith(
377 | 			errorResponse,
378 | 			mockLogger,
379 | 			'Error getting tasks'
380 | 		);
381 | 	});
382 | 
383 | 	test('should handle unexpected errors', () => {
384 | 		const testError = new Error('Unexpected error');
385 | 		mockListTasksDirect.mockImplementationOnce(() => {
386 | 			throw testError;
387 | 		});
388 | 
389 | 		const mockContext = {
390 | 			log: mockLogger,
391 | 			session: { workingDirectory: '/mock/dir' }
392 | 		};
393 | 
394 | 		const args = {
395 | 			status: 'pending',
396 | 			projectRoot: '/mock/project'
397 | 		};
398 | 
399 | 		executeFunction(args, mockContext);
400 | 
401 | 		expect(mockLogger.error).toHaveBeenCalledWith(
402 | 			'Error getting tasks: Unexpected error'
403 | 		);
404 | 		expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
405 | 	});
406 | 
407 | 	test('should pass all parameters correctly', () => {
408 | 		const mockContext = {
409 | 			log: mockLogger,
410 | 			session: { workingDirectory: '/mock/dir' }
411 | 		};
412 | 
413 | 		const args = {
414 | 			status: 'done,pending',
415 | 			withSubtasks: true,
416 | 			file: 'custom-tasks.json',
417 | 			complexityReport: 'custom-report.json',
418 | 			projectRoot: '/mock/project'
419 | 		};
420 | 
421 | 		executeFunction(args, mockContext);
422 | 
423 | 		// Verify path resolution functions were called with correct arguments
424 | 		expect(mockResolveTasksPath).toHaveBeenCalledWith(args, mockLogger);
425 | 		expect(mockResolveComplexityReportPath).toHaveBeenCalledWith(
426 | 			args,
427 | 			mockContext.session
428 | 		);
429 | 
430 | 		// Verify listTasksDirect was called with correct parameters
431 | 		expect(mockListTasksDirect).toHaveBeenCalledWith(
432 | 			{
433 | 				tasksJsonPath: '/mock/project/tasks.json',
434 | 				status: 'done,pending',
435 | 				withSubtasks: true,
436 | 				reportPath: '/mock/project/complexity-report.json'
437 | 			},
438 | 			mockLogger
439 | 		);
440 | 	});
441 | 
442 | 	test('should log task count after successful retrieval', () => {
443 | 		const mockContext = {
444 | 			log: mockLogger,
445 | 			session: { workingDirectory: '/mock/dir' }
446 | 		};
447 | 
448 | 		const args = {
449 | 			status: 'pending',
450 | 			projectRoot: '/mock/project'
451 | 		};
452 | 
453 | 		executeFunction(args, mockContext);
454 | 
455 | 		expect(mockLogger.info).toHaveBeenCalledWith(
456 | 			`Retrieved ${tasksResponse.data.tasks.length} tasks`
457 | 		);
458 | 	});
459 | });
460 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/ai/interfaces/ai-provider.interface.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview AI Provider interface definitions for the tm-core package
  3 |  * This file defines the contract for all AI provider implementations
  4 |  */
  5 | 
  6 | /**
  7 |  * Options for AI completion requests
  8 |  */
  9 | export interface AIOptions {
 10 | 	/** Temperature for response randomness (0.0 to 1.0) */
 11 | 	temperature?: number;
 12 | 	/** Maximum number of tokens to generate */
 13 | 	maxTokens?: number;
 14 | 	/** Whether to use streaming responses */
 15 | 	stream?: boolean;
 16 | 	/** Top-p sampling parameter (0.0 to 1.0) */
 17 | 	topP?: number;
 18 | 	/** Frequency penalty to reduce repetition (-2.0 to 2.0) */
 19 | 	frequencyPenalty?: number;
 20 | 	/** Presence penalty to encourage new topics (-2.0 to 2.0) */
 21 | 	presencePenalty?: number;
 22 | 	/** Stop sequences to halt generation */
 23 | 	stop?: string | string[];
 24 | 	/** Custom system prompt override */
 25 | 	systemPrompt?: string;
 26 | 	/** Request timeout in milliseconds */
 27 | 	timeout?: number;
 28 | 	/** Number of retry attempts on failure */
 29 | 	retries?: number;
 30 | }
 31 | 
 32 | /**
 33 |  * Response from AI completion request
 34 |  */
 35 | export interface AIResponse {
 36 | 	/** Generated text content */
 37 | 	content: string;
 38 | 	/** Token count for the request */
 39 | 	inputTokens: number;
 40 | 	/** Token count for the response */
 41 | 	outputTokens: number;
 42 | 	/** Total tokens used */
 43 | 	totalTokens: number;
 44 | 	/** Cost in USD (if available) */
 45 | 	cost?: number;
 46 | 	/** Model used for generation */
 47 | 	model: string;
 48 | 	/** Provider name */
 49 | 	provider: string;
 50 | 	/** Response timestamp */
 51 | 	timestamp: string;
 52 | 	/** Request duration in milliseconds */
 53 | 	duration: number;
 54 | 	/** Whether the response was cached */
 55 | 	cached?: boolean;
 56 | 	/** Finish reason (completed, length, stop, etc.) */
 57 | 	finishReason?: string;
 58 | }
 59 | 
 60 | /**
 61 |  * AI model information
 62 |  */
 63 | export interface AIModel {
 64 | 	/** Model identifier */
 65 | 	id: string;
 66 | 	/** Human-readable model name */
 67 | 	name: string;
 68 | 	/** Model description */
 69 | 	description?: string;
 70 | 	/** Maximum context length in tokens */
 71 | 	contextLength: number;
 72 | 	/** Input cost per 1K tokens in USD */
 73 | 	inputCostPer1K?: number;
 74 | 	/** Output cost per 1K tokens in USD */
 75 | 	outputCostPer1K?: number;
 76 | 	/** Whether the model supports function calling */
 77 | 	supportsFunctions?: boolean;
 78 | 	/** Whether the model supports vision/image inputs */
 79 | 	supportsVision?: boolean;
 80 | 	/** Whether the model supports streaming */
 81 | 	supportsStreaming?: boolean;
 82 | }
 83 | 
 84 | /**
 85 |  * Provider capabilities and metadata
 86 |  */
 87 | export interface ProviderInfo {
 88 | 	/** Provider name */
 89 | 	name: string;
 90 | 	/** Provider display name */
 91 | 	displayName: string;
 92 | 	/** Provider description */
 93 | 	description?: string;
 94 | 	/** Base API URL */
 95 | 	baseUrl?: string;
 96 | 	/** Available models */
 97 | 	models: AIModel[];
 98 | 	/** Default model ID */
 99 | 	defaultModel: string;
100 | 	/** Whether the provider requires an API key */
101 | 	requiresApiKey: boolean;
102 | 	/** Supported features */
103 | 	features: {
104 | 		streaming?: boolean;
105 | 		functions?: boolean;
106 | 		vision?: boolean;
107 | 		embeddings?: boolean;
108 | 	};
109 | }
110 | 
111 | /**
112 |  * Interface for AI provider implementations
113 |  * All AI providers must implement this interface
114 |  */
115 | export interface IAIProvider {
116 | 	/**
117 | 	 * Generate a text completion from a prompt
118 | 	 * @param prompt - Input prompt text
119 | 	 * @param options - Optional generation parameters
120 | 	 * @returns Promise that resolves to AI response
121 | 	 */
122 | 	generateCompletion(prompt: string, options?: AIOptions): Promise<AIResponse>;
123 | 
124 | 	/**
125 | 	 * Generate a streaming completion (if supported)
126 | 	 * @param prompt - Input prompt text
127 | 	 * @param options - Optional generation parameters
128 | 	 * @returns AsyncIterator of response chunks
129 | 	 */
130 | 	generateStreamingCompletion(
131 | 		prompt: string,
132 | 		options?: AIOptions
133 | 	): AsyncIterator<Partial<AIResponse>>;
134 | 
135 | 	/**
136 | 	 * Calculate token count for given text
137 | 	 * @param text - Text to count tokens for
138 | 	 * @param model - Optional model to use for counting
139 | 	 * @returns Number of tokens
140 | 	 */
141 | 	calculateTokens(text: string, model?: string): number;
142 | 
143 | 	/**
144 | 	 * Get the provider name
145 | 	 * @returns Provider name string
146 | 	 */
147 | 	getName(): string;
148 | 
149 | 	/**
150 | 	 * Get current model being used
151 | 	 * @returns Current model ID
152 | 	 */
153 | 	getModel(): string;
154 | 
155 | 	/**
156 | 	 * Set the model to use for requests
157 | 	 * @param model - Model ID to use
158 | 	 */
159 | 	setModel(model: string): void;
160 | 
161 | 	/**
162 | 	 * Get the default model for this provider
163 | 	 * @returns Default model ID
164 | 	 */
165 | 	getDefaultModel(): string;
166 | 
167 | 	/**
168 | 	 * Check if the provider is available and configured
169 | 	 * @returns Promise that resolves to availability status
170 | 	 */
171 | 	isAvailable(): Promise<boolean>;
172 | 
173 | 	/**
174 | 	 * Get provider information and capabilities
175 | 	 * @returns Provider information object
176 | 	 */
177 | 	getProviderInfo(): ProviderInfo;
178 | 
179 | 	/**
180 | 	 * Get available models for this provider
181 | 	 * @returns Array of available models
182 | 	 */
183 | 	getAvailableModels(): AIModel[];
184 | 
185 | 	/**
186 | 	 * Validate API key or credentials
187 | 	 * @returns Promise that resolves to validation status
188 | 	 */
189 | 	validateCredentials(): Promise<boolean>;
190 | 
191 | 	/**
192 | 	 * Get usage statistics if available
193 | 	 * @returns Promise that resolves to usage stats or null
194 | 	 */
195 | 	getUsageStats(): Promise<ProviderUsageStats | null>;
196 | 
197 | 	/**
198 | 	 * Initialize the provider (set up connections, validate config, etc.)
199 | 	 * @returns Promise that resolves when initialization is complete
200 | 	 */
201 | 	initialize(): Promise<void>;
202 | 
203 | 	/**
204 | 	 * Clean up and close provider connections
205 | 	 * @returns Promise that resolves when cleanup is complete
206 | 	 */
207 | 	close(): Promise<void>;
208 | }
209 | 
210 | /**
211 |  * Usage statistics for a provider
212 |  */
213 | export interface ProviderUsageStats {
214 | 	/** Total requests made */
215 | 	totalRequests: number;
216 | 	/** Total tokens consumed */
217 | 	totalTokens: number;
218 | 	/** Total cost in USD */
219 | 	totalCost: number;
220 | 	/** Requests today */
221 | 	requestsToday: number;
222 | 	/** Tokens used today */
223 | 	tokensToday: number;
224 | 	/** Cost today */
225 | 	costToday: number;
226 | 	/** Average response time in milliseconds */
227 | 	averageResponseTime: number;
228 | 	/** Success rate (0.0 to 1.0) */
229 | 	successRate: number;
230 | 	/** Last request timestamp */
231 | 	lastRequestAt?: string;
232 | 	/** Rate limit information if available */
233 | 	rateLimits?: {
234 | 		requestsPerMinute: number;
235 | 		tokensPerMinute: number;
236 | 		requestsRemaining: number;
237 | 		tokensRemaining: number;
238 | 		resetTime: string;
239 | 	};
240 | }
241 | 
242 | /**
243 |  * Configuration for AI provider instances
244 |  */
245 | export interface AIProviderConfig {
246 | 	/** API key for the provider */
247 | 	apiKey: string;
248 | 	/** Base URL override */
249 | 	baseUrl?: string;
250 | 	/** Default model to use */
251 | 	model?: string;
252 | 	/** Default generation options */
253 | 	defaultOptions?: AIOptions;
254 | 	/** Request timeout in milliseconds */
255 | 	timeout?: number;
256 | 	/** Maximum retry attempts */
257 | 	maxRetries?: number;
258 | 	/** Custom headers to include in requests */
259 | 	headers?: Record<string, string>;
260 | 	/** Enable request/response logging */
261 | 	enableLogging?: boolean;
262 | 	/** Enable usage tracking */
263 | 	enableUsageTracking?: boolean;
264 | }
265 | 
266 | /**
267 |  * Abstract base class for AI provider implementations
268 |  * Provides common functionality and enforces the interface
269 |  */
270 | export abstract class BaseAIProvider implements IAIProvider {
271 | 	protected config: AIProviderConfig;
272 | 	protected currentModel: string;
273 | 	protected usageStats: ProviderUsageStats | null = null;
274 | 
275 | 	constructor(config: AIProviderConfig) {
276 | 		this.config = config;
277 | 		this.currentModel = config.model || this.getDefaultModel();
278 | 
279 | 		if (config.enableUsageTracking) {
280 | 			this.initializeUsageTracking();
281 | 		}
282 | 	}
283 | 
284 | 	// Abstract methods that must be implemented by concrete classes
285 | 	abstract generateCompletion(
286 | 		prompt: string,
287 | 		options?: AIOptions
288 | 	): Promise<AIResponse>;
289 | 	abstract generateStreamingCompletion(
290 | 		prompt: string,
291 | 		options?: AIOptions
292 | 	): AsyncIterator<Partial<AIResponse>>;
293 | 	abstract calculateTokens(text: string, model?: string): number;
294 | 	abstract getName(): string;
295 | 	abstract getDefaultModel(): string;
296 | 	abstract isAvailable(): Promise<boolean>;
297 | 	abstract getProviderInfo(): ProviderInfo;
298 | 	abstract validateCredentials(): Promise<boolean>;
299 | 	abstract initialize(): Promise<void>;
300 | 	abstract close(): Promise<void>;
301 | 
302 | 	// Implemented methods with common functionality
303 | 	getModel(): string {
304 | 		return this.currentModel;
305 | 	}
306 | 
307 | 	setModel(model: string): void {
308 | 		const availableModels = this.getAvailableModels();
309 | 		const modelExists = availableModels.some((m) => m.id === model);
310 | 
311 | 		if (!modelExists) {
312 | 			throw new Error(
313 | 				`Model "${model}" is not available for provider "${this.getName()}"`
314 | 			);
315 | 		}
316 | 
317 | 		this.currentModel = model;
318 | 	}
319 | 
320 | 	getAvailableModels(): AIModel[] {
321 | 		return this.getProviderInfo().models;
322 | 	}
323 | 
324 | 	async getUsageStats(): Promise<ProviderUsageStats | null> {
325 | 		return this.usageStats;
326 | 	}
327 | 
328 | 	/**
329 | 	 * Initialize usage tracking
330 | 	 */
331 | 	protected initializeUsageTracking(): void {
332 | 		this.usageStats = {
333 | 			totalRequests: 0,
334 | 			totalTokens: 0,
335 | 			totalCost: 0,
336 | 			requestsToday: 0,
337 | 			tokensToday: 0,
338 | 			costToday: 0,
339 | 			averageResponseTime: 0,
340 | 			successRate: 1.0
341 | 		};
342 | 	}
343 | 
344 | 	/**
345 | 	 * Update usage statistics after a request
346 | 	 * @param response - AI response to record
347 | 	 * @param duration - Request duration in milliseconds
348 | 	 * @param success - Whether the request was successful
349 | 	 */
350 | 	protected updateUsageStats(
351 | 		response: AIResponse,
352 | 		duration: number,
353 | 		success: boolean
354 | 	): void {
355 | 		if (!this.usageStats) return;
356 | 
357 | 		this.usageStats.totalRequests++;
358 | 		this.usageStats.totalTokens += response.totalTokens;
359 | 
360 | 		if (response.cost) {
361 | 			this.usageStats.totalCost += response.cost;
362 | 		}
363 | 
364 | 		// Update daily stats (simplified - would need proper date tracking)
365 | 		this.usageStats.requestsToday++;
366 | 		this.usageStats.tokensToday += response.totalTokens;
367 | 
368 | 		if (response.cost) {
369 | 			this.usageStats.costToday += response.cost;
370 | 		}
371 | 
372 | 		// Update average response time
373 | 		const totalTime =
374 | 			this.usageStats.averageResponseTime * (this.usageStats.totalRequests - 1);
375 | 		this.usageStats.averageResponseTime =
376 | 			(totalTime + duration) / this.usageStats.totalRequests;
377 | 
378 | 		// Update success rate
379 | 		const successCount = Math.floor(
380 | 			this.usageStats.successRate * (this.usageStats.totalRequests - 1)
381 | 		);
382 | 		const newSuccessCount = successCount + (success ? 1 : 0);
383 | 		this.usageStats.successRate =
384 | 			newSuccessCount / this.usageStats.totalRequests;
385 | 
386 | 		this.usageStats.lastRequestAt = new Date().toISOString();
387 | 	}
388 | 
389 | 	/**
390 | 	 * Merge user options with default options
391 | 	 * @param userOptions - User-provided options
392 | 	 * @returns Merged options object
393 | 	 */
394 | 	protected mergeOptions(userOptions?: AIOptions): AIOptions {
395 | 		return {
396 | 			temperature: 0.7,
397 | 			maxTokens: 2000,
398 | 			stream: false,
399 | 			topP: 1.0,
400 | 			frequencyPenalty: 0.0,
401 | 			presencePenalty: 0.0,
402 | 			timeout: 30000,
403 | 			retries: 3,
404 | 			...this.config.defaultOptions,
405 | 			...userOptions
406 | 		};
407 | 	}
408 | 
409 | 	/**
410 | 	 * Validate prompt input
411 | 	 * @param prompt - Prompt to validate
412 | 	 * @throws Error if prompt is invalid
413 | 	 */
414 | 	protected validatePrompt(prompt: string): void {
415 | 		if (!prompt || typeof prompt !== 'string') {
416 | 			throw new Error('Prompt must be a non-empty string');
417 | 		}
418 | 
419 | 		if (prompt.trim().length === 0) {
420 | 			throw new Error('Prompt cannot be empty or only whitespace');
421 | 		}
422 | 	}
423 | }
424 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/workflow/services/test-result-validator.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import { describe, expect, it } from 'vitest';
  2 | import { TestResultValidator } from './test-result-validator.js';
  3 | import type {
  4 | 	TestPhase,
  5 | 	TestResult,
  6 | 	ValidationResult
  7 | } from './test-result-validator.types.js';
  8 | 
  9 | describe('TestResultValidator - Input Validation', () => {
 10 | 	const validator = new TestResultValidator();
 11 | 
 12 | 	describe('Schema Validation', () => {
 13 | 		it('should validate a valid test result', () => {
 14 | 			const validResult: TestResult = {
 15 | 				total: 10,
 16 | 				passed: 5,
 17 | 				failed: 5,
 18 | 				skipped: 0,
 19 | 				phase: 'RED'
 20 | 			};
 21 | 
 22 | 			const result = validator.validate(validResult);
 23 | 			expect(result.valid).toBe(true);
 24 | 			expect(result.errors).toEqual([]);
 25 | 		});
 26 | 
 27 | 		it('should reject negative test counts', () => {
 28 | 			const invalidResult = {
 29 | 				total: -1,
 30 | 				passed: 0,
 31 | 				failed: 0,
 32 | 				skipped: 0,
 33 | 				phase: 'RED'
 34 | 			};
 35 | 
 36 | 			const result = validator.validate(invalidResult as TestResult);
 37 | 			expect(result.valid).toBe(false);
 38 | 			expect(result.errors.length).toBeGreaterThan(0);
 39 | 		});
 40 | 
 41 | 		it('should reject when totals do not match', () => {
 42 | 			const invalidResult: TestResult = {
 43 | 				total: 10,
 44 | 				passed: 3,
 45 | 				failed: 3,
 46 | 				skipped: 3, // 3 + 3 + 3 = 9, not 10
 47 | 				phase: 'RED'
 48 | 			};
 49 | 
 50 | 			const result = validator.validate(invalidResult);
 51 | 			expect(result.valid).toBe(false);
 52 | 			expect(result.errors).toContain(
 53 | 				'Total tests must equal passed + failed + skipped'
 54 | 			);
 55 | 		});
 56 | 
 57 | 		it('should reject missing required fields', () => {
 58 | 			const invalidResult = {
 59 | 				total: 10,
 60 | 				passed: 5
 61 | 				// missing failed, skipped, phase
 62 | 			};
 63 | 
 64 | 			const result = validator.validate(invalidResult as TestResult);
 65 | 			expect(result.valid).toBe(false);
 66 | 			expect(result.errors.length).toBeGreaterThan(0);
 67 | 		});
 68 | 
 69 | 		it('should accept optional coverage data', () => {
 70 | 			const resultWithCoverage: TestResult = {
 71 | 				total: 10,
 72 | 				passed: 10,
 73 | 				failed: 0,
 74 | 				skipped: 0,
 75 | 				phase: 'GREEN',
 76 | 				coverage: {
 77 | 					line: 85,
 78 | 					branch: 75,
 79 | 					function: 90,
 80 | 					statement: 85
 81 | 				}
 82 | 			};
 83 | 
 84 | 			const result = validator.validate(resultWithCoverage);
 85 | 			expect(result.valid).toBe(true);
 86 | 		});
 87 | 
 88 | 		it('should reject invalid coverage percentages', () => {
 89 | 			const invalidResult: TestResult = {
 90 | 				total: 10,
 91 | 				passed: 10,
 92 | 				failed: 0,
 93 | 				skipped: 0,
 94 | 				phase: 'GREEN',
 95 | 				coverage: {
 96 | 					line: 150, // Invalid: > 100
 97 | 					branch: -10, // Invalid: < 0
 98 | 					function: 90,
 99 | 					statement: 85
100 | 				}
101 | 			};
102 | 
103 | 			const result = validator.validate(invalidResult);
104 | 			expect(result.valid).toBe(false);
105 | 			expect(result.errors.length).toBeGreaterThan(0);
106 | 		});
107 | 
108 | 		it('should reject invalid phase values', () => {
109 | 			const invalidResult = {
110 | 				total: 10,
111 | 				passed: 5,
112 | 				failed: 5,
113 | 				skipped: 0,
114 | 				phase: 'INVALID_PHASE'
115 | 			};
116 | 
117 | 			const result = validator.validate(invalidResult as TestResult);
118 | 			expect(result.valid).toBe(false);
119 | 			expect(result.errors.length).toBeGreaterThan(0);
120 | 		});
121 | 	});
122 | });
123 | 
124 | describe('TestResultValidator - RED Phase Validation', () => {
125 | 	const validator = new TestResultValidator();
126 | 
127 | 	it('should pass validation when RED phase has failures', () => {
128 | 		const redResult: TestResult = {
129 | 			total: 10,
130 | 			passed: 5,
131 | 			failed: 5,
132 | 			skipped: 0,
133 | 			phase: 'RED'
134 | 		};
135 | 
136 | 		const result = validator.validateRedPhase(redResult);
137 | 		expect(result.valid).toBe(true);
138 | 		expect(result.errors).toEqual([]);
139 | 	});
140 | 
141 | 	it('should fail validation when RED phase has zero failures', () => {
142 | 		const redResult: TestResult = {
143 | 			total: 10,
144 | 			passed: 10,
145 | 			failed: 0,
146 | 			skipped: 0,
147 | 			phase: 'RED'
148 | 		};
149 | 
150 | 		const result = validator.validateRedPhase(redResult);
151 | 		expect(result.valid).toBe(false);
152 | 		expect(result.errors).toContain(
153 | 			'RED phase must have at least one failing test'
154 | 		);
155 | 		expect(result.suggestions).toContain(
156 | 			'Write failing tests first to follow TDD workflow'
157 | 		);
158 | 	});
159 | 
160 | 	it('should fail validation when RED phase has empty test suite', () => {
161 | 		const emptyResult: TestResult = {
162 | 			total: 0,
163 | 			passed: 0,
164 | 			failed: 0,
165 | 			skipped: 0,
166 | 			phase: 'RED'
167 | 		};
168 | 
169 | 		const result = validator.validateRedPhase(emptyResult);
170 | 		expect(result.valid).toBe(false);
171 | 		expect(result.errors).toContain('Cannot validate empty test suite');
172 | 		expect(result.suggestions).toContain(
173 | 			'Add at least one test to begin TDD cycle'
174 | 		);
175 | 	});
176 | 
177 | 	it('should propagate base validation errors', () => {
178 | 		const invalidResult: TestResult = {
179 | 			total: 10,
180 | 			passed: 3,
181 | 			failed: 3,
182 | 			skipped: 3, // Total mismatch
183 | 			phase: 'RED'
184 | 		};
185 | 
186 | 		const result = validator.validateRedPhase(invalidResult);
187 | 		expect(result.valid).toBe(false);
188 | 		expect(result.errors).toContain(
189 | 			'Total tests must equal passed + failed + skipped'
190 | 		);
191 | 	});
192 | });
193 | 
194 | describe('TestResultValidator - GREEN Phase Validation', () => {
195 | 	const validator = new TestResultValidator();
196 | 
197 | 	it('should pass validation when GREEN phase has all tests passing', () => {
198 | 		const greenResult: TestResult = {
199 | 			total: 10,
200 | 			passed: 10,
201 | 			failed: 0,
202 | 			skipped: 0,
203 | 			phase: 'GREEN'
204 | 		};
205 | 
206 | 		const result = validator.validateGreenPhase(greenResult);
207 | 		expect(result.valid).toBe(true);
208 | 		expect(result.errors).toEqual([]);
209 | 	});
210 | 
211 | 	it('should fail validation when GREEN phase has failures', () => {
212 | 		const greenResult: TestResult = {
213 | 			total: 10,
214 | 			passed: 5,
215 | 			failed: 5,
216 | 			skipped: 0,
217 | 			phase: 'GREEN'
218 | 		};
219 | 
220 | 		const result = validator.validateGreenPhase(greenResult);
221 | 		expect(result.valid).toBe(false);
222 | 		expect(result.errors).toContain('GREEN phase must have zero failures');
223 | 		expect(result.suggestions).toContain(
224 | 			'Fix implementation to make all tests pass'
225 | 		);
226 | 	});
227 | 
228 | 	it('should fail validation when GREEN phase has no passing tests', () => {
229 | 		const greenResult: TestResult = {
230 | 			total: 5,
231 | 			passed: 0,
232 | 			failed: 0,
233 | 			skipped: 5,
234 | 			phase: 'GREEN'
235 | 		};
236 | 
237 | 		const result = validator.validateGreenPhase(greenResult);
238 | 		expect(result.valid).toBe(false);
239 | 		expect(result.errors).toContain(
240 | 			'GREEN phase must have at least one passing test'
241 | 		);
242 | 	});
243 | 
244 | 	it('should warn when test count decreases', () => {
245 | 		const greenResult: TestResult = {
246 | 			total: 5,
247 | 			passed: 5,
248 | 			failed: 0,
249 | 			skipped: 0,
250 | 			phase: 'GREEN'
251 | 		};
252 | 
253 | 		const result = validator.validateGreenPhase(greenResult, 10);
254 | 		expect(result.valid).toBe(true);
255 | 		expect(result.warnings).toContain('Test count decreased from 10 to 5');
256 | 		expect(result.suggestions).toContain(
257 | 			'Verify that no tests were accidentally removed'
258 | 		);
259 | 	});
260 | 
261 | 	it('should not warn when test count increases', () => {
262 | 		const greenResult: TestResult = {
263 | 			total: 15,
264 | 			passed: 15,
265 | 			failed: 0,
266 | 			skipped: 0,
267 | 			phase: 'GREEN'
268 | 		};
269 | 
270 | 		const result = validator.validateGreenPhase(greenResult, 10);
271 | 		expect(result.valid).toBe(true);
272 | 		expect(result.warnings || []).toEqual([]);
273 | 	});
274 | 
275 | 	it('should propagate base validation errors', () => {
276 | 		const invalidResult: TestResult = {
277 | 			total: 10,
278 | 			passed: 3,
279 | 			failed: 3,
280 | 			skipped: 3, // Total mismatch
281 | 			phase: 'GREEN'
282 | 		};
283 | 
284 | 		const result = validator.validateGreenPhase(invalidResult);
285 | 		expect(result.valid).toBe(false);
286 | 		expect(result.errors).toContain(
287 | 			'Total tests must equal passed + failed + skipped'
288 | 		);
289 | 	});
290 | });
291 | 
292 | describe('TestResultValidator - Coverage Threshold Validation', () => {
293 | 	const validator = new TestResultValidator();
294 | 
295 | 	it('should pass validation when coverage meets thresholds', () => {
296 | 		const result: TestResult = {
297 | 			total: 10,
298 | 			passed: 10,
299 | 			failed: 0,
300 | 			skipped: 0,
301 | 			phase: 'GREEN',
302 | 			coverage: {
303 | 				line: 85,
304 | 				branch: 80,
305 | 				function: 90,
306 | 				statement: 85
307 | 			}
308 | 		};
309 | 
310 | 		const thresholds = {
311 | 			line: 80,
312 | 			branch: 75,
313 | 			function: 85,
314 | 			statement: 80
315 | 		};
316 | 
317 | 		const validationResult = validator.validateCoverage(result, thresholds);
318 | 		expect(validationResult.valid).toBe(true);
319 | 		expect(validationResult.errors).toEqual([]);
320 | 	});
321 | 
322 | 	it('should fail validation when line coverage is below threshold', () => {
323 | 		const result: TestResult = {
324 | 			total: 10,
325 | 			passed: 10,
326 | 			failed: 0,
327 | 			skipped: 0,
328 | 			phase: 'GREEN',
329 | 			coverage: {
330 | 				line: 70,
331 | 				branch: 80,
332 | 				function: 90,
333 | 				statement: 85
334 | 			}
335 | 		};
336 | 
337 | 		const thresholds = {
338 | 			line: 80
339 | 		};
340 | 
341 | 		const validationResult = validator.validateCoverage(result, thresholds);
342 | 		expect(validationResult.valid).toBe(false);
343 | 		expect(validationResult.errors[0]).toContain('line coverage (70% < 80%)');
344 | 		expect(validationResult.suggestions).toContain(
345 | 			'Add more tests to improve code coverage'
346 | 		);
347 | 	});
348 | 
349 | 	it('should fail validation when multiple coverage types are below threshold', () => {
350 | 		const result: TestResult = {
351 | 			total: 10,
352 | 			passed: 10,
353 | 			failed: 0,
354 | 			skipped: 0,
355 | 			phase: 'GREEN',
356 | 			coverage: {
357 | 				line: 70,
358 | 				branch: 60,
359 | 				function: 75,
360 | 				statement: 65
361 | 			}
362 | 		};
363 | 
364 | 		const thresholds = {
365 | 			line: 80,
366 | 			branch: 75,
367 | 			function: 85,
368 | 			statement: 80
369 | 		};
370 | 
371 | 		const validationResult = validator.validateCoverage(result, thresholds);
372 | 		expect(validationResult.valid).toBe(false);
373 | 		expect(validationResult.errors[0]).toContain('line coverage (70% < 80%)');
374 | 		expect(validationResult.errors[0]).toContain('branch coverage (60% < 75%)');
375 | 		expect(validationResult.errors[0]).toContain(
376 | 			'function coverage (75% < 85%)'
377 | 		);
378 | 		expect(validationResult.errors[0]).toContain(
379 | 			'statement coverage (65% < 80%)'
380 | 		);
381 | 	});
382 | 
383 | 	it('should skip validation when no coverage data is provided', () => {
384 | 		const result: TestResult = {
385 | 			total: 10,
386 | 			passed: 10,
387 | 			failed: 0,
388 | 			skipped: 0,
389 | 			phase: 'GREEN'
390 | 		};
391 | 
392 | 		const thresholds = {
393 | 			line: 80,
394 | 			branch: 75
395 | 		};
396 | 
397 | 		const validationResult = validator.validateCoverage(result, thresholds);
398 | 		expect(validationResult.valid).toBe(true);
399 | 		expect(validationResult.errors).toEqual([]);
400 | 	});
401 | 
402 | 	it('should only validate specified threshold types', () => {
403 | 		const result: TestResult = {
404 | 			total: 10,
405 | 			passed: 10,
406 | 			failed: 0,
407 | 			skipped: 0,
408 | 			phase: 'GREEN',
409 | 			coverage: {
410 | 				line: 70,
411 | 				branch: 60,
412 | 				function: 90,
413 | 				statement: 85
414 | 			}
415 | 		};
416 | 
417 | 		const thresholds = {
418 | 			line: 80
419 | 			// Only checking line coverage
420 | 		};
421 | 
422 | 		const validationResult = validator.validateCoverage(result, thresholds);
423 | 		expect(validationResult.valid).toBe(false);
424 | 		expect(validationResult.errors[0]).toContain('line coverage');
425 | 		expect(validationResult.errors[0]).not.toContain('branch coverage');
426 | 	});
427 | 
428 | 	it('should propagate base validation errors', () => {
429 | 		const invalidResult: TestResult = {
430 | 			total: 10,
431 | 			passed: 3,
432 | 			failed: 3,
433 | 			skipped: 3, // Total mismatch
434 | 			phase: 'GREEN',
435 | 			coverage: {
436 | 				line: 90,
437 | 				branch: 90,
438 | 				function: 90,
439 | 				statement: 90
440 | 			}
441 | 		};
442 | 
443 | 		const thresholds = {
444 | 			line: 80
445 | 		};
446 | 
447 | 		const validationResult = validator.validateCoverage(
448 | 			invalidResult,
449 | 			thresholds
450 | 		);
451 | 		expect(validationResult.valid).toBe(false);
452 | 		expect(validationResult.errors).toContain(
453 | 			'Total tests must equal passed + failed + skipped'
454 | 		);
455 | 	});
456 | });
457 | 
```

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

```javascript
  1 | import { jest } from '@jest/globals';
  2 | import fs from 'fs';
  3 | import path from 'path';
  4 | import os from 'os';
  5 | 
  6 | // Mock external modules
  7 | jest.mock('child_process', () => ({
  8 | 	execSync: jest.fn()
  9 | }));
 10 | 
 11 | jest.mock('readline', () => ({
 12 | 	createInterface: jest.fn(() => ({
 13 | 		question: jest.fn(),
 14 | 		close: jest.fn()
 15 | 	}))
 16 | }));
 17 | 
 18 | // Mock figlet for banner display
 19 | jest.mock('figlet', () => ({
 20 | 	default: {
 21 | 		textSync: jest.fn(() => 'Task Master')
 22 | 	}
 23 | }));
 24 | 
 25 | // Mock console methods
 26 | jest.mock('console', () => ({
 27 | 	log: jest.fn(),
 28 | 	info: jest.fn(),
 29 | 	warn: jest.fn(),
 30 | 	error: jest.fn(),
 31 | 	clear: jest.fn()
 32 | }));
 33 | 
 34 | describe('Windsurf Rules File Handling', () => {
 35 | 	let tempDir;
 36 | 
 37 | 	beforeEach(() => {
 38 | 		jest.clearAllMocks();
 39 | 
 40 | 		// Create a temporary directory for testing
 41 | 		tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
 42 | 
 43 | 		// Spy on fs methods
 44 | 		jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
 45 | 		jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
 46 | 			if (filePath.toString().includes('.windsurfrules')) {
 47 | 				return 'Existing windsurf rules content';
 48 | 			}
 49 | 			return '{}';
 50 | 		});
 51 | 		jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
 52 | 			// Mock specific file existence checks
 53 | 			if (filePath.toString().includes('package.json')) {
 54 | 				return true;
 55 | 			}
 56 | 			return false;
 57 | 		});
 58 | 		jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
 59 | 		jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
 60 | 	});
 61 | 
 62 | 	afterEach(() => {
 63 | 		// Clean up the temporary directory
 64 | 		try {
 65 | 			fs.rmSync(tempDir, { recursive: true, force: true });
 66 | 		} catch (err) {
 67 | 			console.error(`Error cleaning up: ${err.message}`);
 68 | 		}
 69 | 	});
 70 | 
 71 | 	// Test function that simulates the behavior of .windsurfrules handling
 72 | 	function mockCopyTemplateFile(templateName, targetPath) {
 73 | 		if (templateName === 'windsurfrules') {
 74 | 			const filename = path.basename(targetPath);
 75 | 
 76 | 			if (filename === '.windsurfrules') {
 77 | 				if (fs.existsSync(targetPath)) {
 78 | 					// Should append content when file exists
 79 | 					const existingContent = fs.readFileSync(targetPath, 'utf8');
 80 | 					const updatedContent =
 81 | 						existingContent.trim() +
 82 | 						'\n\n# Added by Claude Task Master - Development Workflow Rules\n\n' +
 83 | 						'New content';
 84 | 					fs.writeFileSync(targetPath, updatedContent);
 85 | 					return;
 86 | 				}
 87 | 			}
 88 | 
 89 | 			// If file doesn't exist, create it normally
 90 | 			fs.writeFileSync(targetPath, 'New content');
 91 | 		}
 92 | 	}
 93 | 
 94 | 	test('creates .windsurfrules when it does not exist', () => {
 95 | 		// Arrange
 96 | 		const targetPath = path.join(tempDir, '.windsurfrules');
 97 | 
 98 | 		// Act
 99 | 		mockCopyTemplateFile('windsurfrules', targetPath);
100 | 
101 | 		// Assert
102 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(targetPath, 'New content');
103 | 	});
104 | 
105 | 	test('appends content to existing .windsurfrules', () => {
106 | 		// Arrange
107 | 		const targetPath = path.join(tempDir, '.windsurfrules');
108 | 		const existingContent = 'Existing windsurf rules content';
109 | 
110 | 		// Override the existsSync mock just for this test
111 | 		fs.existsSync.mockReturnValueOnce(true); // Target file exists
112 | 		fs.readFileSync.mockReturnValueOnce(existingContent);
113 | 
114 | 		// Act
115 | 		mockCopyTemplateFile('windsurfrules', targetPath);
116 | 
117 | 		// Assert
118 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(
119 | 			targetPath,
120 | 			expect.stringContaining(existingContent)
121 | 		);
122 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(
123 | 			targetPath,
124 | 			expect.stringContaining('Added by Claude Task Master')
125 | 		);
126 | 	});
127 | 
128 | 	test('includes .windsurfrules in project structure creation', () => {
129 | 		// This test verifies the expected behavior by using a mock implementation
130 | 		// that represents how createProjectStructure should work
131 | 
132 | 		// Mock implementation of createProjectStructure
133 | 		function mockCreateProjectStructure(projectName) {
134 | 			// Copy template files including .windsurfrules
135 | 			mockCopyTemplateFile(
136 | 				'windsurfrules',
137 | 				path.join(tempDir, '.windsurfrules')
138 | 			);
139 | 		}
140 | 
141 | 		// Act - call our mock implementation
142 | 		mockCreateProjectStructure('test-project');
143 | 
144 | 		// Assert - verify that .windsurfrules was created
145 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(
146 | 			path.join(tempDir, '.windsurfrules'),
147 | 			expect.any(String)
148 | 		);
149 | 	});
150 | });
151 | 
152 | // New test suite for MCP Configuration Handling
153 | describe('MCP Configuration Handling', () => {
154 | 	let tempDir;
155 | 
156 | 	beforeEach(() => {
157 | 		jest.clearAllMocks();
158 | 
159 | 		// Create a temporary directory for testing
160 | 		tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
161 | 
162 | 		// Spy on fs methods
163 | 		jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
164 | 		jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
165 | 			if (filePath.toString().includes('mcp.json')) {
166 | 				return JSON.stringify({
167 | 					mcpServers: {
168 | 						'existing-server': {
169 | 							command: 'node',
170 | 							args: ['server.js']
171 | 						}
172 | 					}
173 | 				});
174 | 			}
175 | 			return '{}';
176 | 		});
177 | 		jest.spyOn(fs, 'existsSync').mockImplementation((filePath) => {
178 | 			// Return true for specific paths to test different scenarios
179 | 			if (filePath.toString().includes('package.json')) {
180 | 				return true;
181 | 			}
182 | 			// Default to false for other paths
183 | 			return false;
184 | 		});
185 | 		jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
186 | 		jest.spyOn(fs, 'copyFileSync').mockImplementation(() => {});
187 | 	});
188 | 
189 | 	afterEach(() => {
190 | 		// Clean up the temporary directory
191 | 		try {
192 | 			fs.rmSync(tempDir, { recursive: true, force: true });
193 | 		} catch (err) {
194 | 			console.error(`Error cleaning up: ${err.message}`);
195 | 		}
196 | 	});
197 | 
198 | 	// Test function that simulates the behavior of setupMCPConfiguration
199 | 	function mockSetupMCPConfiguration(targetDir, projectName) {
200 | 		const mcpDirPath = path.join(targetDir, '.cursor');
201 | 		const mcpJsonPath = path.join(mcpDirPath, 'mcp.json');
202 | 
203 | 		// Create .cursor directory if it doesn't exist
204 | 		if (!fs.existsSync(mcpDirPath)) {
205 | 			fs.mkdirSync(mcpDirPath, { recursive: true });
206 | 		}
207 | 
208 | 		// New MCP config to be added - references the installed package
209 | 		const newMCPServer = {
210 | 			'task-master-ai': {
211 | 				command: 'npx',
212 | 				args: ['task-master-ai', 'mcp-server']
213 | 			}
214 | 		};
215 | 
216 | 		// Check if mcp.json already exists
217 | 		if (fs.existsSync(mcpJsonPath)) {
218 | 			try {
219 | 				// Read existing config
220 | 				const mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'));
221 | 
222 | 				// Initialize mcpServers if it doesn't exist
223 | 				if (!mcpConfig.mcpServers) {
224 | 					mcpConfig.mcpServers = {};
225 | 				}
226 | 
227 | 				// Add the taskmaster-ai server if it doesn't exist
228 | 				if (!mcpConfig.mcpServers['task-master-ai']) {
229 | 					mcpConfig.mcpServers['task-master-ai'] =
230 | 						newMCPServer['task-master-ai'];
231 | 				}
232 | 
233 | 				// Write the updated configuration
234 | 				fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 4));
235 | 			} catch (error) {
236 | 				// Create new configuration on error
237 | 				const newMCPConfig = {
238 | 					mcpServers: newMCPServer
239 | 				};
240 | 
241 | 				fs.writeFileSync(mcpJsonPath, JSON.stringify(newMCPConfig, null, 4));
242 | 			}
243 | 		} else {
244 | 			// If mcp.json doesn't exist, create it
245 | 			const newMCPConfig = {
246 | 				mcpServers: newMCPServer
247 | 			};
248 | 
249 | 			fs.writeFileSync(mcpJsonPath, JSON.stringify(newMCPConfig, null, 4));
250 | 		}
251 | 	}
252 | 
253 | 	test('creates mcp.json when it does not exist', () => {
254 | 		// Arrange
255 | 		const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
256 | 
257 | 		// Act
258 | 		mockSetupMCPConfiguration(tempDir, 'test-project');
259 | 
260 | 		// Assert
261 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(
262 | 			mcpJsonPath,
263 | 			expect.stringContaining('task-master-ai')
264 | 		);
265 | 
266 | 		// Should create a proper structure with mcpServers key
267 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(
268 | 			mcpJsonPath,
269 | 			expect.stringContaining('mcpServers')
270 | 		);
271 | 
272 | 		// Should reference npx command
273 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(
274 | 			mcpJsonPath,
275 | 			expect.stringContaining('npx')
276 | 		);
277 | 	});
278 | 
279 | 	test('updates existing mcp.json by adding new server', () => {
280 | 		// Arrange
281 | 		const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
282 | 
283 | 		// Override the existsSync mock to simulate mcp.json exists
284 | 		fs.existsSync.mockImplementation((filePath) => {
285 | 			if (filePath.toString().includes('mcp.json')) {
286 | 				return true;
287 | 			}
288 | 			return false;
289 | 		});
290 | 
291 | 		// Act
292 | 		mockSetupMCPConfiguration(tempDir, 'test-project');
293 | 
294 | 		// Assert
295 | 		// Should preserve existing server
296 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(
297 | 			mcpJsonPath,
298 | 			expect.stringContaining('existing-server')
299 | 		);
300 | 
301 | 		// Should add our new server
302 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(
303 | 			mcpJsonPath,
304 | 			expect.stringContaining('task-master-ai')
305 | 		);
306 | 	});
307 | 
308 | 	test('handles JSON parsing errors by creating new mcp.json', () => {
309 | 		// Arrange
310 | 		const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
311 | 
312 | 		// Override existsSync to say mcp.json exists
313 | 		fs.existsSync.mockImplementation((filePath) => {
314 | 			if (filePath.toString().includes('mcp.json')) {
315 | 				return true;
316 | 			}
317 | 			return false;
318 | 		});
319 | 
320 | 		// But make readFileSync return invalid JSON
321 | 		fs.readFileSync.mockImplementation((filePath) => {
322 | 			if (filePath.toString().includes('mcp.json')) {
323 | 				return '{invalid json';
324 | 			}
325 | 			return '{}';
326 | 		});
327 | 
328 | 		// Act
329 | 		mockSetupMCPConfiguration(tempDir, 'test-project');
330 | 
331 | 		// Assert
332 | 		// Should create a new valid JSON file with our server
333 | 		expect(fs.writeFileSync).toHaveBeenCalledWith(
334 | 			mcpJsonPath,
335 | 			expect.stringContaining('task-master-ai')
336 | 		);
337 | 	});
338 | 
339 | 	test('does not modify existing server configuration if it already exists', () => {
340 | 		// Arrange
341 | 		const mcpJsonPath = path.join(tempDir, '.cursor', 'mcp.json');
342 | 
343 | 		// Override existsSync to say mcp.json exists
344 | 		fs.existsSync.mockImplementation((filePath) => {
345 | 			if (filePath.toString().includes('mcp.json')) {
346 | 				return true;
347 | 			}
348 | 			return false;
349 | 		});
350 | 
351 | 		// Return JSON that already has task-master-ai
352 | 		fs.readFileSync.mockImplementation((filePath) => {
353 | 			if (filePath.toString().includes('mcp.json')) {
354 | 				return JSON.stringify({
355 | 					mcpServers: {
356 | 						'existing-server': {
357 | 							command: 'node',
358 | 							args: ['server.js']
359 | 						},
360 | 						'task-master-ai': {
361 | 							command: 'custom',
362 | 							args: ['custom-args']
363 | 						}
364 | 					}
365 | 				});
366 | 			}
367 | 			return '{}';
368 | 		});
369 | 
370 | 		// Spy to check what's written
371 | 		const writeFileSyncSpy = jest.spyOn(fs, 'writeFileSync');
372 | 
373 | 		// Act
374 | 		mockSetupMCPConfiguration(tempDir, 'test-project');
375 | 
376 | 		// Assert
377 | 		// Verify the written data contains the original taskmaster configuration
378 | 		const dataWritten = JSON.parse(writeFileSyncSpy.mock.calls[0][1]);
379 | 		expect(dataWritten.mcpServers['task-master-ai'].command).toBe('custom');
380 | 		expect(dataWritten.mcpServers['task-master-ai'].args).toContain(
381 | 			'custom-args'
382 | 		);
383 | 	});
384 | 
385 | 	test('creates the .cursor directory if it doesnt exist', () => {
386 | 		// Arrange
387 | 		const cursorDirPath = path.join(tempDir, '.cursor');
388 | 
389 | 		// Make sure it looks like the directory doesn't exist
390 | 		fs.existsSync.mockReturnValue(false);
391 | 
392 | 		// Act
393 | 		mockSetupMCPConfiguration(tempDir, 'test-project');
394 | 
395 | 		// Assert
396 | 		expect(fs.mkdirSync).toHaveBeenCalledWith(cursorDirPath, {
397 | 			recursive: true
398 | 		});
399 | 	});
400 | });
401 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/tests/integration/list-tasks.test.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview End-to-end integration test for listTasks functionality
  3 |  */
  4 | 
  5 | import { promises as fs } from 'node:fs';
  6 | import os from 'node:os';
  7 | import path from 'node:path';
  8 | import { afterEach, beforeEach, describe, expect, it } from 'vitest';
  9 | import {
 10 | 	type Task,
 11 | 	type TaskMasterCore,
 12 | 	type TaskStatus,
 13 | 	createTaskMasterCore
 14 | } from '../../src/index';
 15 | 
 16 | describe('TaskMasterCore - listTasks E2E', () => {
 17 | 	let tmpDir: string;
 18 | 	let tmCore: TaskMasterCore;
 19 | 
 20 | 	// Sample tasks data
 21 | 	const sampleTasks: Task[] = [
 22 | 		{
 23 | 			id: '1',
 24 | 			title: 'Setup project',
 25 | 			description: 'Initialize the project structure',
 26 | 			status: 'done',
 27 | 			priority: 'high',
 28 | 			dependencies: [],
 29 | 			details: 'Create all necessary directories and config files',
 30 | 			testStrategy: 'Manual verification',
 31 | 			subtasks: [
 32 | 				{
 33 | 					id: 1,
 34 | 					parentId: '1',
 35 | 					title: 'Create directories',
 36 | 					description: 'Create project directories',
 37 | 					status: 'done',
 38 | 					priority: 'high',
 39 | 					dependencies: [],
 40 | 					details: 'Create src, tests, docs directories',
 41 | 					testStrategy: 'Check directories exist'
 42 | 				},
 43 | 				{
 44 | 					id: 2,
 45 | 					parentId: '1',
 46 | 					title: 'Initialize package.json',
 47 | 					description: 'Create package.json file',
 48 | 					status: 'done',
 49 | 					priority: 'high',
 50 | 					dependencies: [],
 51 | 					details: 'Run npm init',
 52 | 					testStrategy: 'Verify package.json exists'
 53 | 				}
 54 | 			],
 55 | 			tags: ['setup', 'infrastructure']
 56 | 		},
 57 | 		{
 58 | 			id: '2',
 59 | 			title: 'Implement core features',
 60 | 			description: 'Build the main functionality',
 61 | 			status: 'in-progress',
 62 | 			priority: 'high',
 63 | 			dependencies: ['1'],
 64 | 			details: 'Implement all core business logic',
 65 | 			testStrategy: 'Unit tests for all features',
 66 | 			subtasks: [],
 67 | 			tags: ['feature', 'core'],
 68 | 			assignee: 'developer1'
 69 | 		},
 70 | 		{
 71 | 			id: '3',
 72 | 			title: 'Write documentation',
 73 | 			description: 'Create user and developer docs',
 74 | 			status: 'pending',
 75 | 			priority: 'medium',
 76 | 			dependencies: ['2'],
 77 | 			details: 'Write comprehensive documentation',
 78 | 			testStrategy: 'Review by team',
 79 | 			subtasks: [],
 80 | 			tags: ['documentation'],
 81 | 			complexity: 'simple'
 82 | 		},
 83 | 		{
 84 | 			id: '4',
 85 | 			title: 'Performance optimization',
 86 | 			description: 'Optimize for speed and efficiency',
 87 | 			status: 'blocked',
 88 | 			priority: 'low',
 89 | 			dependencies: ['2'],
 90 | 			details: 'Profile and optimize bottlenecks',
 91 | 			testStrategy: 'Performance benchmarks',
 92 | 			subtasks: [],
 93 | 			assignee: 'developer2',
 94 | 			complexity: 'complex'
 95 | 		},
 96 | 		{
 97 | 			id: '5',
 98 | 			title: 'Security audit',
 99 | 			description: 'Review security vulnerabilities',
100 | 			status: 'deferred',
101 | 			priority: 'critical',
102 | 			dependencies: [],
103 | 			details: 'Complete security assessment',
104 | 			testStrategy: 'Security scanning tools',
105 | 			subtasks: [],
106 | 			tags: ['security', 'audit']
107 | 		}
108 | 	];
109 | 
110 | 	beforeEach(async () => {
111 | 		// Create temp directory for testing
112 | 		tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tm-core-test-'));
113 | 
114 | 		// Create .taskmaster/tasks directory
115 | 		const tasksDir = path.join(tmpDir, '.taskmaster', 'tasks');
116 | 		await fs.mkdir(tasksDir, { recursive: true });
117 | 
118 | 		// Write sample tasks.json
119 | 		const tasksFile = path.join(tasksDir, 'tasks.json');
120 | 		const tasksData = {
121 | 			tasks: sampleTasks,
122 | 			metadata: {
123 | 				version: '1.0.0',
124 | 				lastModified: new Date().toISOString(),
125 | 				taskCount: sampleTasks.length,
126 | 				completedCount: 1
127 | 			}
128 | 		};
129 | 		await fs.writeFile(tasksFile, JSON.stringify(tasksData, null, 2));
130 | 
131 | 		// Create TaskMasterCore instance
132 | 		tmCore = createTaskMasterCore(tmpDir);
133 | 		await tmCore.initialize();
134 | 	});
135 | 
136 | 	afterEach(async () => {
137 | 		// Cleanup
138 | 		if (tmCore) {
139 | 			await tmCore.close();
140 | 		}
141 | 
142 | 		// Remove temp directory
143 | 		await fs.rm(tmpDir, { recursive: true, force: true });
144 | 	});
145 | 
146 | 	describe('Basic listing', () => {
147 | 		it('should list all tasks', async () => {
148 | 			const result = await tmCore.listTasks();
149 | 
150 | 			expect(result.tasks).toHaveLength(5);
151 | 			expect(result.total).toBe(5);
152 | 			expect(result.filtered).toBe(5);
153 | 			expect(result.tag).toBeUndefined();
154 | 		});
155 | 
156 | 		it('should include subtasks by default', async () => {
157 | 			const result = await tmCore.listTasks();
158 | 			const setupTask = result.tasks.find((t) => t.id === '1');
159 | 
160 | 			expect(setupTask?.subtasks).toHaveLength(2);
161 | 			expect(setupTask?.subtasks[0].title).toBe('Create directories');
162 | 		});
163 | 
164 | 		it('should exclude subtasks when requested', async () => {
165 | 			const result = await tmCore.listTasks({ includeSubtasks: false });
166 | 			const setupTask = result.tasks.find((t) => t.id === '1');
167 | 
168 | 			expect(setupTask?.subtasks).toHaveLength(0);
169 | 		});
170 | 	});
171 | 
172 | 	describe('Filtering', () => {
173 | 		it('should filter by status', async () => {
174 | 			const result = await tmCore.listTasks({
175 | 				filter: { status: 'done' }
176 | 			});
177 | 
178 | 			expect(result.filtered).toBe(1);
179 | 			expect(result.tasks[0].id).toBe('1');
180 | 		});
181 | 
182 | 		it('should filter by multiple statuses', async () => {
183 | 			const result = await tmCore.listTasks({
184 | 				filter: { status: ['done', 'in-progress'] }
185 | 			});
186 | 
187 | 			expect(result.filtered).toBe(2);
188 | 			const ids = result.tasks.map((t) => t.id);
189 | 			expect(ids).toContain('1');
190 | 			expect(ids).toContain('2');
191 | 		});
192 | 
193 | 		it('should filter by priority', async () => {
194 | 			const result = await tmCore.listTasks({
195 | 				filter: { priority: 'high' }
196 | 			});
197 | 
198 | 			expect(result.filtered).toBe(2);
199 | 		});
200 | 
201 | 		it('should filter by tags', async () => {
202 | 			const result = await tmCore.listTasks({
203 | 				filter: { tags: ['setup'] }
204 | 			});
205 | 
206 | 			expect(result.filtered).toBe(1);
207 | 			expect(result.tasks[0].id).toBe('1');
208 | 		});
209 | 
210 | 		it('should filter by assignee', async () => {
211 | 			const result = await tmCore.listTasks({
212 | 				filter: { assignee: 'developer1' }
213 | 			});
214 | 
215 | 			expect(result.filtered).toBe(1);
216 | 			expect(result.tasks[0].id).toBe('2');
217 | 		});
218 | 
219 | 		it('should filter by complexity', async () => {
220 | 			const result = await tmCore.listTasks({
221 | 				filter: { complexity: 'complex' }
222 | 			});
223 | 
224 | 			expect(result.filtered).toBe(1);
225 | 			expect(result.tasks[0].id).toBe('4');
226 | 		});
227 | 
228 | 		it('should filter by search term', async () => {
229 | 			const result = await tmCore.listTasks({
230 | 				filter: { search: 'documentation' }
231 | 			});
232 | 
233 | 			expect(result.filtered).toBe(1);
234 | 			expect(result.tasks[0].id).toBe('3');
235 | 		});
236 | 
237 | 		it('should filter by hasSubtasks', async () => {
238 | 			const withSubtasks = await tmCore.listTasks({
239 | 				filter: { hasSubtasks: true }
240 | 			});
241 | 
242 | 			expect(withSubtasks.filtered).toBe(1);
243 | 			expect(withSubtasks.tasks[0].id).toBe('1');
244 | 
245 | 			const withoutSubtasks = await tmCore.listTasks({
246 | 				filter: { hasSubtasks: false }
247 | 			});
248 | 
249 | 			expect(withoutSubtasks.filtered).toBe(4);
250 | 		});
251 | 
252 | 		it('should handle combined filters', async () => {
253 | 			const result = await tmCore.listTasks({
254 | 				filter: {
255 | 					priority: ['high', 'critical'],
256 | 					status: ['pending', 'deferred']
257 | 				}
258 | 			});
259 | 
260 | 			expect(result.filtered).toBe(1);
261 | 			expect(result.tasks[0].id).toBe('5'); // Critical priority, deferred status
262 | 		});
263 | 	});
264 | 
265 | 	describe('Helper methods', () => {
266 | 		it('should get task by ID', async () => {
267 | 			const task = await tmCore.getTask('2');
268 | 
269 | 			expect(task).not.toBeNull();
270 | 			expect(task?.title).toBe('Implement core features');
271 | 		});
272 | 
273 | 		it('should return null for non-existent task', async () => {
274 | 			const task = await tmCore.getTask('999');
275 | 
276 | 			expect(task).toBeNull();
277 | 		});
278 | 
279 | 		it('should get tasks by status', async () => {
280 | 			const pendingTasks = await tmCore.getTasksByStatus('pending');
281 | 
282 | 			expect(pendingTasks).toHaveLength(1);
283 | 			expect(pendingTasks[0].id).toBe('3');
284 | 
285 | 			const multipleTasks = await tmCore.getTasksByStatus(['done', 'blocked']);
286 | 
287 | 			expect(multipleTasks).toHaveLength(2);
288 | 		});
289 | 
290 | 		it('should get task statistics', async () => {
291 | 			const stats = await tmCore.getTaskStats();
292 | 
293 | 			expect(stats.total).toBe(5);
294 | 			expect(stats.byStatus.done).toBe(1);
295 | 			expect(stats.byStatus['in-progress']).toBe(1);
296 | 			expect(stats.byStatus.pending).toBe(1);
297 | 			expect(stats.byStatus.blocked).toBe(1);
298 | 			expect(stats.byStatus.deferred).toBe(1);
299 | 			expect(stats.byStatus.cancelled).toBe(0);
300 | 			expect(stats.byStatus.review).toBe(0);
301 | 			expect(stats.withSubtasks).toBe(1);
302 | 			expect(stats.blocked).toBe(1);
303 | 		});
304 | 	});
305 | 
306 | 	describe('Error handling', () => {
307 | 		it('should handle missing tasks file gracefully', async () => {
308 | 			// Create new instance with empty directory
309 | 			const emptyDir = await fs.mkdtemp(path.join(os.tmpdir(), 'tm-empty-'));
310 | 			const emptyCore = createTaskMasterCore(emptyDir);
311 | 
312 | 			try {
313 | 				const result = await emptyCore.listTasks();
314 | 
315 | 				expect(result.tasks).toHaveLength(0);
316 | 				expect(result.total).toBe(0);
317 | 				expect(result.filtered).toBe(0);
318 | 			} finally {
319 | 				await emptyCore.close();
320 | 				await fs.rm(emptyDir, { recursive: true, force: true });
321 | 			}
322 | 		});
323 | 
324 | 		it('should validate task entities', async () => {
325 | 			// Write invalid task data
326 | 			const invalidDir = await fs.mkdtemp(
327 | 				path.join(os.tmpdir(), 'tm-invalid-')
328 | 			);
329 | 			const tasksDir = path.join(invalidDir, '.taskmaster', 'tasks');
330 | 			await fs.mkdir(tasksDir, { recursive: true });
331 | 
332 | 			const invalidData = {
333 | 				tasks: [
334 | 					{
335 | 						id: '', // Invalid: empty ID
336 | 						title: 'Test',
337 | 						description: 'Test',
338 | 						status: 'done',
339 | 						priority: 'high',
340 | 						dependencies: [],
341 | 						details: 'Test',
342 | 						testStrategy: 'Test',
343 | 						subtasks: []
344 | 					}
345 | 				],
346 | 				metadata: {
347 | 					version: '1.0.0',
348 | 					lastModified: new Date().toISOString(),
349 | 					taskCount: 1,
350 | 					completedCount: 0
351 | 				}
352 | 			};
353 | 
354 | 			await fs.writeFile(
355 | 				path.join(tasksDir, 'tasks.json'),
356 | 				JSON.stringify(invalidData)
357 | 			);
358 | 
359 | 			const invalidCore = createTaskMasterCore(invalidDir);
360 | 
361 | 			try {
362 | 				await expect(invalidCore.listTasks()).rejects.toThrow();
363 | 			} finally {
364 | 				await invalidCore.close();
365 | 				await fs.rm(invalidDir, { recursive: true, force: true });
366 | 			}
367 | 		});
368 | 	});
369 | 
370 | 	describe('Tags support', () => {
371 | 		beforeEach(async () => {
372 | 			// Create tasks for a different tag
373 | 			const taggedTasks = [
374 | 				{
375 | 					id: 'tag-1',
376 | 					title: 'Tagged task',
377 | 					description: 'Task with tag',
378 | 					status: 'pending' as TaskStatus,
379 | 					priority: 'medium' as const,
380 | 					dependencies: [],
381 | 					details: 'Tagged task details',
382 | 					testStrategy: 'Test',
383 | 					subtasks: []
384 | 				}
385 | 			];
386 | 
387 | 			const tagFile = path.join(
388 | 				tmpDir,
389 | 				'.taskmaster',
390 | 				'tasks',
391 | 				'feature-branch.json'
392 | 			);
393 | 			await fs.writeFile(
394 | 				tagFile,
395 | 				JSON.stringify({
396 | 					tasks: taggedTasks,
397 | 					metadata: {
398 | 						version: '1.0.0',
399 | 						lastModified: new Date().toISOString(),
400 | 						taskCount: 1,
401 | 						completedCount: 0
402 | 					}
403 | 				})
404 | 			);
405 | 		});
406 | 
407 | 		it('should list tasks for specific tag', async () => {
408 | 			const result = await tmCore.listTasks({ tag: 'feature-branch' });
409 | 
410 | 			expect(result.tasks).toHaveLength(1);
411 | 			expect(result.tasks[0].id).toBe('tag-1');
412 | 			expect(result.tag).toBe('feature-branch');
413 | 		});
414 | 
415 | 		it('should list default tasks when no tag specified', async () => {
416 | 			const result = await tmCore.listTasks();
417 | 
418 | 			expect(result.tasks).toHaveLength(5);
419 | 			expect(result.tasks[0].id).toBe('1');
420 | 		});
421 | 	});
422 | });
423 | 
```
Page 31/69FirstPrevNextLast