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 |
```