This is page 22 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
--------------------------------------------------------------------------------
/.github/scripts/auto-close-duplicates.mjs:
--------------------------------------------------------------------------------
```
1 | #!/usr/bin/env node
2 |
3 | async function githubRequest(endpoint, token, method = 'GET', body) {
4 | const response = await fetch(`https://api.github.com${endpoint}`, {
5 | method,
6 | headers: {
7 | Authorization: `Bearer ${token}`,
8 | Accept: 'application/vnd.github.v3+json',
9 | 'User-Agent': 'auto-close-duplicates-script',
10 | ...(body && { 'Content-Type': 'application/json' })
11 | },
12 | ...(body && { body: JSON.stringify(body) })
13 | });
14 |
15 | if (!response.ok) {
16 | throw new Error(
17 | `GitHub API request failed: ${response.status} ${response.statusText}`
18 | );
19 | }
20 |
21 | return response.json();
22 | }
23 |
24 | function extractDuplicateIssueNumber(commentBody) {
25 | const match = commentBody.match(/#(\d+)/);
26 | return match ? parseInt(match[1], 10) : null;
27 | }
28 |
29 | async function closeIssueAsDuplicate(
30 | owner,
31 | repo,
32 | issueNumber,
33 | duplicateOfNumber,
34 | token
35 | ) {
36 | await githubRequest(
37 | `/repos/${owner}/${repo}/issues/${issueNumber}`,
38 | token,
39 | 'PATCH',
40 | {
41 | state: 'closed',
42 | state_reason: 'not_planned',
43 | labels: ['duplicate']
44 | }
45 | );
46 |
47 | await githubRequest(
48 | `/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
49 | token,
50 | 'POST',
51 | {
52 | body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
53 |
54 | If this is incorrect, please re-open this issue or create a new one.
55 |
56 | 🤖 Generated with [Task Master Bot]`
57 | }
58 | );
59 | }
60 |
61 | async function autoCloseDuplicates() {
62 | console.log('[DEBUG] Starting auto-close duplicates script');
63 |
64 | const token = process.env.GITHUB_TOKEN;
65 | if (!token) {
66 | throw new Error('GITHUB_TOKEN environment variable is required');
67 | }
68 | console.log('[DEBUG] GitHub token found');
69 |
70 | const owner = process.env.GITHUB_REPOSITORY_OWNER || 'eyaltoledano';
71 | const repo = process.env.GITHUB_REPOSITORY_NAME || 'claude-task-master';
72 | console.log(`[DEBUG] Repository: ${owner}/${repo}`);
73 |
74 | const threeDaysAgo = new Date();
75 | threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
76 | console.log(
77 | `[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`
78 | );
79 |
80 | console.log('[DEBUG] Fetching open issues created more than 3 days ago...');
81 | const allIssues = [];
82 | let page = 1;
83 | const perPage = 100;
84 |
85 | const MAX_PAGES = 50; // Increase limit for larger repos
86 | let foundRecentIssue = false;
87 |
88 | while (true) {
89 | const pageIssues = await githubRequest(
90 | `/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}&sort=created&direction=desc`,
91 | token
92 | );
93 |
94 | if (pageIssues.length === 0) break;
95 |
96 | // Filter for issues created more than 3 days ago
97 | const oldEnoughIssues = pageIssues.filter(
98 | (issue) => new Date(issue.created_at) <= threeDaysAgo
99 | );
100 |
101 | allIssues.push(...oldEnoughIssues);
102 |
103 | // If all issues on this page are newer than 3 days, we can stop
104 | if (oldEnoughIssues.length === 0 && page === 1) {
105 | foundRecentIssue = true;
106 | break;
107 | }
108 |
109 | // If we found some old issues but not all, continue to next page
110 | // as there might be more old issues
111 | page++;
112 |
113 | // Safety limit to avoid infinite loops
114 | if (page > MAX_PAGES) {
115 | console.log(`[WARNING] Reached maximum page limit of ${MAX_PAGES}`);
116 | break;
117 | }
118 | }
119 |
120 | const issues = allIssues;
121 | console.log(`[DEBUG] Found ${issues.length} open issues`);
122 |
123 | let processedCount = 0;
124 | let candidateCount = 0;
125 |
126 | for (const issue of issues) {
127 | processedCount++;
128 | console.log(
129 | `[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`
130 | );
131 |
132 | console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
133 | const comments = await githubRequest(
134 | `/repos/${owner}/${repo}/issues/${issue.number}/comments`,
135 | token
136 | );
137 | console.log(
138 | `[DEBUG] Issue #${issue.number} has ${comments.length} comments`
139 | );
140 |
141 | const dupeComments = comments.filter(
142 | (comment) =>
143 | comment.body.includes('Found') &&
144 | comment.body.includes('possible duplicate') &&
145 | comment.user.type === 'Bot'
146 | );
147 | console.log(
148 | `[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`
149 | );
150 |
151 | if (dupeComments.length === 0) {
152 | console.log(
153 | `[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`
154 | );
155 | continue;
156 | }
157 |
158 | const lastDupeComment = dupeComments[dupeComments.length - 1];
159 | const dupeCommentDate = new Date(lastDupeComment.created_at);
160 | console.log(
161 | `[DEBUG] Issue #${
162 | issue.number
163 | } - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`
164 | );
165 |
166 | if (dupeCommentDate > threeDaysAgo) {
167 | console.log(
168 | `[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`
169 | );
170 | continue;
171 | }
172 | console.log(
173 | `[DEBUG] Issue #${
174 | issue.number
175 | } - duplicate comment is old enough (${Math.floor(
176 | (Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24)
177 | )} days)`
178 | );
179 |
180 | const commentsAfterDupe = comments.filter(
181 | (comment) => new Date(comment.created_at) > dupeCommentDate
182 | );
183 | console.log(
184 | `[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`
185 | );
186 |
187 | if (commentsAfterDupe.length > 0) {
188 | console.log(
189 | `[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`
190 | );
191 | continue;
192 | }
193 |
194 | console.log(
195 | `[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`
196 | );
197 | const reactions = await githubRequest(
198 | `/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
199 | token
200 | );
201 | console.log(
202 | `[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`
203 | );
204 |
205 | const authorThumbsDown = reactions.some(
206 | (reaction) =>
207 | reaction.user.id === issue.user.id && reaction.content === '-1'
208 | );
209 | console.log(
210 | `[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`
211 | );
212 |
213 | if (authorThumbsDown) {
214 | console.log(
215 | `[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`
216 | );
217 | continue;
218 | }
219 |
220 | const duplicateIssueNumber = extractDuplicateIssueNumber(
221 | lastDupeComment.body
222 | );
223 | if (!duplicateIssueNumber) {
224 | console.log(
225 | `[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`
226 | );
227 | continue;
228 | }
229 |
230 | candidateCount++;
231 | const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
232 |
233 | try {
234 | console.log(
235 | `[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`
236 | );
237 | await closeIssueAsDuplicate(
238 | owner,
239 | repo,
240 | issue.number,
241 | duplicateIssueNumber,
242 | token
243 | );
244 | console.log(
245 | `[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`
246 | );
247 | } catch (error) {
248 | console.error(
249 | `[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`
250 | );
251 | }
252 | }
253 |
254 | console.log(
255 | `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`
256 | );
257 | }
258 |
259 | autoCloseDuplicates().catch(console.error);
260 |
```
--------------------------------------------------------------------------------
/apps/extension/src/webview/components/TaskEditModal.tsx:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Task Edit Modal Component
3 | */
4 |
5 | import React, { useState, useEffect, useRef } from 'react';
6 | import { Button } from '@/components/ui/button';
7 | import { Label } from '@/components/ui/label';
8 | import { Textarea } from '@/components/ui/textarea';
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuTrigger
14 | } from '@/components/ui/dropdown-menu';
15 | import type { TaskMasterTask, TaskUpdates } from '../types';
16 |
17 | interface TaskEditModalProps {
18 | task: TaskMasterTask;
19 | onSave: (taskId: string, updates: TaskUpdates) => Promise<void>;
20 | onCancel: () => void;
21 | }
22 |
23 | export const TaskEditModal: React.FC<TaskEditModalProps> = ({
24 | task,
25 | onSave,
26 | onCancel
27 | }) => {
28 | const [updates, setUpdates] = useState<TaskUpdates>({
29 | title: task.title,
30 | description: task.description || '',
31 | details: task.details || '',
32 | testStrategy: task.testStrategy || '',
33 | priority: task.priority,
34 | dependencies: task.dependencies || []
35 | });
36 | const [isSaving, setIsSaving] = useState(false);
37 | const formRef = useRef<HTMLFormElement>(null);
38 | const titleInputRef = useRef<HTMLInputElement>(null);
39 |
40 | // Focus title input on mount
41 | useEffect(() => {
42 | titleInputRef.current?.focus();
43 | titleInputRef.current?.select();
44 | }, []);
45 |
46 | const handleSubmit = async (e?: React.FormEvent) => {
47 | e?.preventDefault();
48 | setIsSaving(true);
49 |
50 | try {
51 | await onSave(task.id, updates);
52 | } catch (error) {
53 | console.error('Failed to save task:', error);
54 | } finally {
55 | setIsSaving(false);
56 | }
57 | };
58 |
59 | const hasChanges = () => {
60 | return (
61 | updates.title !== task.title ||
62 | updates.description !== (task.description || '') ||
63 | updates.details !== (task.details || '') ||
64 | updates.testStrategy !== (task.testStrategy || '') ||
65 | updates.priority !== task.priority ||
66 | JSON.stringify(updates.dependencies) !==
67 | JSON.stringify(task.dependencies || [])
68 | );
69 | };
70 |
71 | return (
72 | <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
73 | <div className="bg-vscode-editor-background border border-vscode-border rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
74 | {/* Header */}
75 | <div className="flex items-center justify-between p-4 border-b border-vscode-border">
76 | <h2 className="text-lg font-semibold">Edit Task #{task.id}</h2>
77 | <button
78 | onClick={onCancel}
79 | className="text-vscode-foreground/50 hover:text-vscode-foreground transition-colors"
80 | >
81 | <svg
82 | className="w-5 h-5"
83 | fill="none"
84 | stroke="currentColor"
85 | viewBox="0 0 24 24"
86 | >
87 | <path
88 | strokeLinecap="round"
89 | strokeLinejoin="round"
90 | strokeWidth={2}
91 | d="M6 18L18 6M6 6l12 12"
92 | />
93 | </svg>
94 | </button>
95 | </div>
96 |
97 | {/* Form */}
98 | <form
99 | ref={formRef}
100 | onSubmit={handleSubmit}
101 | className="flex-1 overflow-y-auto p-4 space-y-4"
102 | >
103 | {/* Title */}
104 | <div className="space-y-2">
105 | <Label htmlFor="title">Title</Label>
106 | <input
107 | ref={titleInputRef}
108 | id="title"
109 | type="text"
110 | value={updates.title || ''}
111 | onChange={(e) =>
112 | setUpdates({ ...updates, title: e.target.value })
113 | }
114 | className="w-full px-3 py-2 bg-vscode-input border border-vscode-border rounded-md text-vscode-foreground focus:outline-none focus:ring-2 focus:ring-vscode-focusBorder"
115 | placeholder="Task title"
116 | />
117 | </div>
118 |
119 | {/* Priority */}
120 | <div className="space-y-2">
121 | <Label htmlFor="priority">Priority</Label>
122 | <DropdownMenu>
123 | <DropdownMenuTrigger asChild>
124 | <Button variant="outline" className="w-full justify-between">
125 | <span className="capitalize">{updates.priority}</span>
126 | <svg
127 | className="w-4 h-4"
128 | fill="none"
129 | stroke="currentColor"
130 | viewBox="0 0 24 24"
131 | >
132 | <path
133 | strokeLinecap="round"
134 | strokeLinejoin="round"
135 | strokeWidth={2}
136 | d="M19 9l-7 7-7-7"
137 | />
138 | </svg>
139 | </Button>
140 | </DropdownMenuTrigger>
141 | <DropdownMenuContent className="w-full">
142 | <DropdownMenuItem
143 | onClick={() => setUpdates({ ...updates, priority: 'high' })}
144 | >
145 | High
146 | </DropdownMenuItem>
147 | <DropdownMenuItem
148 | onClick={() => setUpdates({ ...updates, priority: 'medium' })}
149 | >
150 | Medium
151 | </DropdownMenuItem>
152 | <DropdownMenuItem
153 | onClick={() => setUpdates({ ...updates, priority: 'low' })}
154 | >
155 | Low
156 | </DropdownMenuItem>
157 | </DropdownMenuContent>
158 | </DropdownMenu>
159 | </div>
160 |
161 | {/* Description */}
162 | <div className="space-y-2">
163 | <Label htmlFor="description">Description</Label>
164 | <Textarea
165 | id="description"
166 | value={updates.description || ''}
167 | onChange={(e) =>
168 | setUpdates({ ...updates, description: e.target.value })
169 | }
170 | className="min-h-[80px]"
171 | placeholder="Brief description of the task"
172 | />
173 | </div>
174 |
175 | {/* Details */}
176 | <div className="space-y-2">
177 | <Label htmlFor="details">Implementation Details</Label>
178 | <Textarea
179 | id="details"
180 | value={updates.details || ''}
181 | onChange={(e) =>
182 | setUpdates({ ...updates, details: e.target.value })
183 | }
184 | className="min-h-[120px]"
185 | placeholder="Technical details and implementation notes"
186 | />
187 | </div>
188 |
189 | {/* Test Strategy */}
190 | <div className="space-y-2">
191 | <Label htmlFor="testStrategy">Test Strategy</Label>
192 | <Textarea
193 | id="testStrategy"
194 | value={updates.testStrategy || ''}
195 | onChange={(e) =>
196 | setUpdates({ ...updates, testStrategy: e.target.value })
197 | }
198 | className="min-h-[80px]"
199 | placeholder="How to test this task"
200 | />
201 | </div>
202 |
203 | {/* Dependencies */}
204 | <div className="space-y-2">
205 | <Label htmlFor="dependencies">
206 | Dependencies (comma-separated task IDs)
207 | </Label>
208 | <input
209 | id="dependencies"
210 | type="text"
211 | value={updates.dependencies?.join(', ') || ''}
212 | onChange={(e) =>
213 | setUpdates({
214 | ...updates,
215 | dependencies: e.target.value
216 | .split(',')
217 | .map((d) => d.trim())
218 | .filter(Boolean)
219 | })
220 | }
221 | className="w-full px-3 py-2 bg-vscode-input border border-vscode-border rounded-md text-vscode-foreground focus:outline-none focus:ring-2 focus:ring-vscode-focusBorder"
222 | placeholder="e.g., 1, 2.1, 3"
223 | />
224 | </div>
225 | </form>
226 |
227 | {/* Footer */}
228 | <div className="flex items-center justify-end gap-2 p-4 border-t border-vscode-border">
229 | <Button variant="outline" onClick={onCancel} disabled={isSaving}>
230 | Cancel
231 | </Button>
232 | <Button
233 | onClick={() => handleSubmit()}
234 | disabled={isSaving || !hasChanges()}
235 | >
236 | {isSaving ? 'Saving...' : 'Save Changes'}
237 | </Button>
238 | </div>
239 | </div>
240 | </div>
241 | );
242 | };
243 |
```
--------------------------------------------------------------------------------
/tests/unit/ui.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * UI module tests
3 | */
4 |
5 | import { jest } from '@jest/globals';
6 | import {
7 | getStatusWithColor,
8 | formatDependenciesWithStatus,
9 | createProgressBar,
10 | getComplexityWithColor
11 | } from '../../scripts/modules/ui.js';
12 | import { sampleTasks } from '../fixtures/sample-tasks.js';
13 |
14 | // Mock dependencies
15 | jest.mock('chalk', () => {
16 | const origChalkFn = (text) => text;
17 | const chalk = origChalkFn;
18 | chalk.green = (text) => text; // Return text as-is for status functions
19 | chalk.yellow = (text) => text;
20 | chalk.red = (text) => text;
21 | chalk.cyan = (text) => text;
22 | chalk.blue = (text) => text;
23 | chalk.gray = (text) => text;
24 | chalk.white = (text) => text;
25 | chalk.bold = (text) => text;
26 | chalk.dim = (text) => text;
27 |
28 | // Add hex and other methods
29 | chalk.hex = () => origChalkFn;
30 | chalk.rgb = () => origChalkFn;
31 |
32 | return chalk;
33 | });
34 |
35 | jest.mock('figlet', () => ({
36 | textSync: jest.fn(() => 'Task Master Banner')
37 | }));
38 |
39 | jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`));
40 |
41 | jest.mock('ora', () =>
42 | jest.fn(() => ({
43 | start: jest.fn(),
44 | succeed: jest.fn(),
45 | fail: jest.fn(),
46 | stop: jest.fn()
47 | }))
48 | );
49 |
50 | jest.mock('cli-table3', () =>
51 | jest.fn().mockImplementation(() => ({
52 | push: jest.fn(),
53 | toString: jest.fn(() => 'Table Content')
54 | }))
55 | );
56 |
57 | jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
58 |
59 | jest.mock('../../scripts/modules/utils.js', () => ({
60 | CONFIG: {
61 | projectName: 'Test Project',
62 | projectVersion: '1.0.0'
63 | },
64 | log: jest.fn(),
65 | findTaskById: jest.fn(),
66 | readJSON: jest.fn(),
67 | readComplexityReport: jest.fn(),
68 | truncate: jest.fn((text) => text)
69 | }));
70 |
71 | jest.mock('../../scripts/modules/task-manager.js', () => ({
72 | findNextTask: jest.fn(),
73 | analyzeTaskComplexity: jest.fn()
74 | }));
75 |
76 | describe('UI Module', () => {
77 | beforeEach(() => {
78 | jest.clearAllMocks();
79 | });
80 |
81 | describe('getStatusWithColor function', () => {
82 | test('should return done status with emoji for console output', () => {
83 | const result = getStatusWithColor('done');
84 | expect(result).toMatch(/done/);
85 | expect(result).toContain('✓');
86 | });
87 |
88 | test('should return pending status with emoji for console output', () => {
89 | const result = getStatusWithColor('pending');
90 | expect(result).toMatch(/pending/);
91 | expect(result).toContain('○');
92 | });
93 |
94 | test('should return deferred status with emoji for console output', () => {
95 | const result = getStatusWithColor('deferred');
96 | expect(result).toMatch(/deferred/);
97 | expect(result).toContain('x');
98 | });
99 |
100 | test('should return in-progress status with emoji for console output', () => {
101 | const result = getStatusWithColor('in-progress');
102 | expect(result).toMatch(/in-progress/);
103 | expect(result).toContain('🔄');
104 | });
105 |
106 | test('should return unknown status with emoji for console output', () => {
107 | const result = getStatusWithColor('unknown');
108 | expect(result).toMatch(/unknown/);
109 | expect(result).toContain('❌');
110 | });
111 |
112 | test('should use simple icons when forTable is true', () => {
113 | const doneResult = getStatusWithColor('done', true);
114 | expect(doneResult).toMatch(/done/);
115 | expect(doneResult).toContain('✓');
116 |
117 | const pendingResult = getStatusWithColor('pending', true);
118 | expect(pendingResult).toMatch(/pending/);
119 | expect(pendingResult).toContain('○');
120 |
121 | const inProgressResult = getStatusWithColor('in-progress', true);
122 | expect(inProgressResult).toMatch(/in-progress/);
123 | expect(inProgressResult).toContain('►');
124 |
125 | const deferredResult = getStatusWithColor('deferred', true);
126 | expect(deferredResult).toMatch(/deferred/);
127 | expect(deferredResult).toContain('x');
128 | });
129 | });
130 |
131 | describe('formatDependenciesWithStatus function', () => {
132 | test('should format dependencies as plain IDs when forConsole is false (default)', () => {
133 | const dependencies = [1, 2, 3];
134 | const allTasks = [
135 | { id: 1, status: 'done' },
136 | { id: 2, status: 'pending' },
137 | { id: 3, status: 'deferred' }
138 | ];
139 |
140 | const result = formatDependenciesWithStatus(dependencies, allTasks);
141 |
142 | // With recent changes, we expect just plain IDs when forConsole is false
143 | expect(result).toBe('1, 2, 3');
144 | });
145 |
146 | test('should format dependencies with status indicators when forConsole is true', () => {
147 | const dependencies = [1, 2, 3];
148 | const allTasks = [
149 | { id: 1, status: 'done' },
150 | { id: 2, status: 'pending' },
151 | { id: 3, status: 'deferred' }
152 | ];
153 |
154 | const result = formatDependenciesWithStatus(dependencies, allTasks, true);
155 |
156 | // We can't test for exact color formatting due to our chalk mocks
157 | // Instead, test that the result contains all the expected IDs
158 | expect(result).toContain('1');
159 | expect(result).toContain('2');
160 | expect(result).toContain('3');
161 |
162 | // Test that it's a comma-separated list
163 | expect(result.split(', ').length).toBe(3);
164 | });
165 |
166 | test('should return "None" for empty dependencies', () => {
167 | const result = formatDependenciesWithStatus([], []);
168 | expect(result).toBe('None');
169 | });
170 |
171 | test('should handle missing tasks in the task list', () => {
172 | const dependencies = [1, 999];
173 | const allTasks = [{ id: 1, status: 'done' }];
174 |
175 | const result = formatDependenciesWithStatus(dependencies, allTasks);
176 | expect(result).toBe('1, 999 (Not found)');
177 | });
178 | });
179 |
180 | describe('createProgressBar function', () => {
181 | test('should create a progress bar with the correct percentage', () => {
182 | const result = createProgressBar(50, 10, {
183 | pending: 20,
184 | 'in-progress': 15,
185 | blocked: 5
186 | });
187 | expect(result).toContain('50%');
188 | });
189 |
190 | test('should handle 0% progress', () => {
191 | const result = createProgressBar(0, 10);
192 | expect(result).toContain('0%');
193 | });
194 |
195 | test('should handle 100% progress', () => {
196 | const result = createProgressBar(100, 10);
197 | expect(result).toContain('100%');
198 | });
199 |
200 | test('should handle invalid percentages by clamping', () => {
201 | const result1 = createProgressBar(0, 10);
202 | expect(result1).toContain('0%');
203 |
204 | const result2 = createProgressBar(100, 10);
205 | expect(result2).toContain('100%');
206 | });
207 |
208 | test('should support status breakdown in the progress bar', () => {
209 | const result = createProgressBar(30, 10, {
210 | pending: 30,
211 | 'in-progress': 20,
212 | blocked: 10,
213 | deferred: 5,
214 | cancelled: 5
215 | });
216 |
217 | expect(result).toContain('40%');
218 | });
219 | });
220 |
221 | describe('getComplexityWithColor function', () => {
222 | test('should return high complexity in red', () => {
223 | const result = getComplexityWithColor(8);
224 | expect(result).toMatch(/8/);
225 | expect(result).toContain('●');
226 | });
227 |
228 | test('should return medium complexity in yellow', () => {
229 | const result = getComplexityWithColor(5);
230 | expect(result).toMatch(/5/);
231 | expect(result).toContain('●');
232 | });
233 |
234 | test('should return low complexity in green', () => {
235 | const result = getComplexityWithColor(3);
236 | expect(result).toMatch(/3/);
237 | expect(result).toContain('●');
238 | });
239 |
240 | test('should handle non-numeric inputs', () => {
241 | const result = getComplexityWithColor('high');
242 | expect(result).toMatch(/high/);
243 | expect(result).toContain('●');
244 | });
245 | });
246 | });
247 |
```
--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/research.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * research.js
3 | * Direct function implementation for AI-powered research queries
4 | */
5 |
6 | import path from 'path';
7 | import { performResearch } from '../../../../scripts/modules/task-manager.js';
8 | import {
9 | enableSilentMode,
10 | disableSilentMode
11 | } from '../../../../scripts/modules/utils.js';
12 | import { createLogWrapper } from '../../tools/utils.js';
13 |
14 | /**
15 | * Direct function wrapper for performing AI-powered research with project context.
16 | *
17 | * @param {Object} args - Command arguments
18 | * @param {string} args.query - Research query/prompt (required)
19 | * @param {string} [args.taskIds] - Comma-separated list of task/subtask IDs for context
20 | * @param {string} [args.filePaths] - Comma-separated list of file paths for context
21 | * @param {string} [args.customContext] - Additional custom context text
22 | * @param {boolean} [args.includeProjectTree=false] - Include project file tree in context
23 | * @param {string} [args.detailLevel='medium'] - Detail level: 'low', 'medium', 'high'
24 | * @param {string} [args.saveTo] - Automatically save to task/subtask ID (e.g., "15" or "15.2")
25 | * @param {boolean} [args.saveToFile=false] - Save research results to .taskmaster/docs/research/ directory
26 | * @param {string} [args.projectRoot] - Project root path
27 | * @param {string} [args.tag] - Tag for the task (optional)
28 | * @param {Object} log - Logger object
29 | * @param {Object} context - Additional context (session)
30 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
31 | */
32 | export async function researchDirect(args, log, context = {}) {
33 | // Destructure expected args
34 | const {
35 | query,
36 | taskIds,
37 | filePaths,
38 | customContext,
39 | includeProjectTree = false,
40 | detailLevel = 'medium',
41 | saveTo,
42 | saveToFile = false,
43 | projectRoot,
44 | tag
45 | } = args;
46 | const { session } = context; // Destructure session from context
47 |
48 | // Enable silent mode to prevent console logs from interfering with JSON response
49 | enableSilentMode();
50 |
51 | // Create logger wrapper using the utility
52 | const mcpLog = createLogWrapper(log);
53 |
54 | try {
55 | // Check required parameters
56 | if (!query || typeof query !== 'string' || query.trim().length === 0) {
57 | log.error('Missing or invalid required parameter: query');
58 | disableSilentMode();
59 | return {
60 | success: false,
61 | error: {
62 | code: 'MISSING_PARAMETER',
63 | message:
64 | 'The query parameter is required and must be a non-empty string'
65 | }
66 | };
67 | }
68 |
69 | // Parse comma-separated task IDs if provided
70 | const parsedTaskIds = taskIds
71 | ? taskIds
72 | .split(',')
73 | .map((id) => id.trim())
74 | .filter((id) => id.length > 0)
75 | : [];
76 |
77 | // Parse comma-separated file paths if provided
78 | const parsedFilePaths = filePaths
79 | ? filePaths
80 | .split(',')
81 | .map((path) => path.trim())
82 | .filter((path) => path.length > 0)
83 | : [];
84 |
85 | // Validate detail level
86 | const validDetailLevels = ['low', 'medium', 'high'];
87 | if (!validDetailLevels.includes(detailLevel)) {
88 | log.error(`Invalid detail level: ${detailLevel}`);
89 | disableSilentMode();
90 | return {
91 | success: false,
92 | error: {
93 | code: 'INVALID_PARAMETER',
94 | message: `Detail level must be one of: ${validDetailLevels.join(', ')}`
95 | }
96 | };
97 | }
98 |
99 | log.info(
100 | `Performing research query: "${query.substring(0, 100)}${query.length > 100 ? '...' : ''}", ` +
101 | `taskIds: [${parsedTaskIds.join(', ')}], ` +
102 | `filePaths: [${parsedFilePaths.join(', ')}], ` +
103 | `detailLevel: ${detailLevel}, ` +
104 | `includeProjectTree: ${includeProjectTree}, ` +
105 | `projectRoot: ${projectRoot}`
106 | );
107 |
108 | // Prepare options for the research function
109 | const researchOptions = {
110 | taskIds: parsedTaskIds,
111 | filePaths: parsedFilePaths,
112 | customContext: customContext || '',
113 | includeProjectTree,
114 | detailLevel,
115 | projectRoot,
116 | tag,
117 | saveToFile
118 | };
119 |
120 | // Prepare context for the research function
121 | const researchContext = {
122 | session,
123 | mcpLog,
124 | commandName: 'research',
125 | outputType: 'mcp'
126 | };
127 |
128 | // Call the performResearch function
129 | const result = await performResearch(
130 | query.trim(),
131 | researchOptions,
132 | researchContext,
133 | 'json', // outputFormat - use 'json' to suppress CLI UI
134 | false // allowFollowUp - disable for MCP calls
135 | );
136 |
137 | // Auto-save to task/subtask if requested
138 | if (saveTo) {
139 | try {
140 | const isSubtask = saveTo.includes('.');
141 |
142 | // Format research content for saving
143 | const researchContent = `## Research Query: ${query.trim()}
144 |
145 | **Detail Level:** ${result.detailLevel}
146 | **Context Size:** ${result.contextSize} characters
147 | **Timestamp:** ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}
148 |
149 | ### Results
150 |
151 | ${result.result}`;
152 |
153 | if (isSubtask) {
154 | // Save to subtask
155 | const { updateSubtaskById } = await import(
156 | '../../../../scripts/modules/task-manager/update-subtask-by-id.js'
157 | );
158 |
159 | const tasksPath = path.join(
160 | projectRoot,
161 | '.taskmaster',
162 | 'tasks',
163 | 'tasks.json'
164 | );
165 | await updateSubtaskById(
166 | tasksPath,
167 | saveTo,
168 | researchContent,
169 | false, // useResearch = false for simple append
170 | {
171 | session,
172 | mcpLog,
173 | commandName: 'research-save',
174 | outputType: 'mcp',
175 | projectRoot,
176 | tag
177 | },
178 | 'json'
179 | );
180 |
181 | log.info(`Research saved to subtask ${saveTo}`);
182 | } else {
183 | // Save to task
184 | const updateTaskById = (
185 | await import(
186 | '../../../../scripts/modules/task-manager/update-task-by-id.js'
187 | )
188 | ).default;
189 |
190 | const taskIdNum = parseInt(saveTo, 10);
191 | const tasksPath = path.join(
192 | projectRoot,
193 | '.taskmaster',
194 | 'tasks',
195 | 'tasks.json'
196 | );
197 | await updateTaskById(
198 | tasksPath,
199 | taskIdNum,
200 | researchContent,
201 | false, // useResearch = false for simple append
202 | {
203 | session,
204 | mcpLog,
205 | commandName: 'research-save',
206 | outputType: 'mcp',
207 | projectRoot,
208 | tag
209 | },
210 | 'json',
211 | true // appendMode = true
212 | );
213 |
214 | log.info(`Research saved to task ${saveTo}`);
215 | }
216 | } catch (saveError) {
217 | log.warn(`Error saving research to task/subtask: ${saveError.message}`);
218 | }
219 | }
220 |
221 | // Restore normal logging
222 | disableSilentMode();
223 |
224 | return {
225 | success: true,
226 | data: {
227 | query: result.query,
228 | result: result.result,
229 | contextSize: result.contextSize,
230 | contextTokens: result.contextTokens,
231 | tokenBreakdown: result.tokenBreakdown,
232 | systemPromptTokens: result.systemPromptTokens,
233 | userPromptTokens: result.userPromptTokens,
234 | totalInputTokens: result.totalInputTokens,
235 | detailLevel: result.detailLevel,
236 | telemetryData: result.telemetryData,
237 | tagInfo: result.tagInfo,
238 | savedFilePath: result.savedFilePath
239 | }
240 | };
241 | } catch (error) {
242 | // Make sure to restore normal logging even if there's an error
243 | disableSilentMode();
244 |
245 | log.error(`Error in researchDirect: ${error.message}`);
246 | return {
247 | success: false,
248 | error: {
249 | code: error.code || 'RESEARCH_ERROR',
250 | message: error.message
251 | }
252 | };
253 | }
254 | }
255 |
```
--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/analyze-task-complexity.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Direct function wrapper for analyzeTaskComplexity
3 | */
4 |
5 | import analyzeTaskComplexity from '../../../../scripts/modules/task-manager/analyze-task-complexity.js';
6 | import {
7 | enableSilentMode,
8 | disableSilentMode,
9 | isSilentMode
10 | } from '../../../../scripts/modules/utils.js';
11 | import fs from 'fs';
12 | import { createLogWrapper } from '../../tools/utils.js'; // Import the new utility
13 |
14 | /**
15 | * Analyze task complexity and generate recommendations
16 | * @param {Object} args - Function arguments
17 | * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
18 | * @param {string} args.outputPath - Explicit absolute path to save the report.
19 | * @param {string|number} [args.threshold] - Minimum complexity score to recommend expansion (1-10)
20 | * @param {boolean} [args.research] - Use Perplexity AI for research-backed complexity analysis
21 | * @param {string} [args.ids] - Comma-separated list of task IDs to analyze
22 | * @param {number} [args.from] - Starting task ID in a range to analyze
23 | * @param {number} [args.to] - Ending task ID in a range to analyze
24 | * @param {string} [args.projectRoot] - Project root path.
25 | * @param {string} [args.tag] - Tag for the task (optional)
26 | * @param {Object} log - Logger object
27 | * @param {Object} [context={}] - Context object containing session data
28 | * @param {Object} [context.session] - MCP session object
29 | * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
30 | */
31 | export async function analyzeTaskComplexityDirect(args, log, context = {}) {
32 | const { session } = context;
33 | const {
34 | tasksJsonPath,
35 | outputPath,
36 | threshold,
37 | research,
38 | projectRoot,
39 | ids,
40 | from,
41 | to,
42 | tag
43 | } = args;
44 |
45 | const logWrapper = createLogWrapper(log);
46 |
47 | // --- Initial Checks (remain the same) ---
48 | try {
49 | log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
50 |
51 | if (!tasksJsonPath) {
52 | log.error('analyzeTaskComplexityDirect called without tasksJsonPath');
53 | return {
54 | success: false,
55 | error: {
56 | code: 'MISSING_ARGUMENT',
57 | message: 'tasksJsonPath is required'
58 | }
59 | };
60 | }
61 | if (!outputPath) {
62 | log.error('analyzeTaskComplexityDirect called without outputPath');
63 | return {
64 | success: false,
65 | error: { code: 'MISSING_ARGUMENT', message: 'outputPath is required' }
66 | };
67 | }
68 |
69 | const tasksPath = tasksJsonPath;
70 | const resolvedOutputPath = outputPath;
71 |
72 | log.info(`Analyzing task complexity from: ${tasksPath}`);
73 | log.info(`Output report will be saved to: ${resolvedOutputPath}`);
74 |
75 | if (ids) {
76 | log.info(`Analyzing specific task IDs: ${ids}`);
77 | } else if (from || to) {
78 | const fromStr = from !== undefined ? from : 'first';
79 | const toStr = to !== undefined ? to : 'last';
80 | log.info(`Analyzing tasks in range: ${fromStr} to ${toStr}`);
81 | }
82 |
83 | if (research) {
84 | log.info('Using research role for complexity analysis');
85 | }
86 |
87 | // Prepare options for the core function - REMOVED mcpLog and session here
88 | const coreOptions = {
89 | file: tasksJsonPath,
90 | output: outputPath,
91 | threshold: threshold,
92 | research: research === true, // Ensure boolean
93 | projectRoot: projectRoot, // Pass projectRoot here
94 | id: ids, // Pass the ids parameter to the core function as 'id'
95 | from: from, // Pass from parameter
96 | to: to, // Pass to parameter
97 | tag // forward tag
98 | };
99 | // --- End Initial Checks ---
100 |
101 | // --- Silent Mode and Logger Wrapper ---
102 | const wasSilent = isSilentMode();
103 | if (!wasSilent) {
104 | enableSilentMode(); // Still enable silent mode as a backup
105 | }
106 |
107 | let report;
108 | let coreResult;
109 |
110 | try {
111 | // --- Call Core Function (Pass context separately) ---
112 | // Pass coreOptions as the first argument
113 | // Pass context object { session, mcpLog } as the second argument
114 | coreResult = await analyzeTaskComplexity(coreOptions, {
115 | session,
116 | mcpLog: logWrapper,
117 | commandName: 'analyze-complexity',
118 | outputType: 'mcp',
119 | projectRoot,
120 | tag
121 | });
122 | report = coreResult.report;
123 | } catch (error) {
124 | log.error(
125 | `Error in analyzeTaskComplexity core function: ${error.message}`
126 | );
127 | // Restore logging if we changed it
128 | if (!wasSilent && isSilentMode()) {
129 | disableSilentMode();
130 | }
131 | return {
132 | success: false,
133 | error: {
134 | code: 'ANALYZE_CORE_ERROR',
135 | message: `Error running core complexity analysis: ${error.message}`
136 | }
137 | };
138 | } finally {
139 | // Always restore normal logging in finally block if we enabled silent mode
140 | if (!wasSilent && isSilentMode()) {
141 | disableSilentMode();
142 | }
143 | }
144 |
145 | // --- Result Handling (remains largely the same) ---
146 | // Verify the report file was created (core function writes it)
147 | if (!fs.existsSync(resolvedOutputPath)) {
148 | return {
149 | success: false,
150 | error: {
151 | code: 'ANALYZE_REPORT_MISSING', // Specific code
152 | message:
153 | 'Analysis completed but no report file was created at the expected path.'
154 | }
155 | };
156 | }
157 |
158 | if (
159 | !coreResult ||
160 | !coreResult.report ||
161 | typeof coreResult.report !== 'object'
162 | ) {
163 | log.error(
164 | 'Core analysis function returned an invalid or undefined response.'
165 | );
166 | return {
167 | success: false,
168 | error: {
169 | code: 'INVALID_CORE_RESPONSE',
170 | message: 'Core analysis function returned an invalid response.'
171 | }
172 | };
173 | }
174 |
175 | try {
176 | // Ensure complexityAnalysis exists and is an array
177 | const analysisArray = Array.isArray(coreResult.report.complexityAnalysis)
178 | ? coreResult.report.complexityAnalysis
179 | : [];
180 |
181 | // Count tasks by complexity (remains the same)
182 | const highComplexityTasks = analysisArray.filter(
183 | (t) => t.complexityScore >= 8
184 | ).length;
185 | const mediumComplexityTasks = analysisArray.filter(
186 | (t) => t.complexityScore >= 5 && t.complexityScore < 8
187 | ).length;
188 | const lowComplexityTasks = analysisArray.filter(
189 | (t) => t.complexityScore < 5
190 | ).length;
191 |
192 | return {
193 | success: true,
194 | data: {
195 | message: `Task complexity analysis complete. Report saved to ${outputPath}`,
196 | reportPath: outputPath,
197 | reportSummary: {
198 | taskCount: analysisArray.length,
199 | highComplexityTasks,
200 | mediumComplexityTasks,
201 | lowComplexityTasks
202 | },
203 | fullReport: coreResult.report,
204 | telemetryData: coreResult.telemetryData,
205 | tagInfo: coreResult.tagInfo
206 | }
207 | };
208 | } catch (parseError) {
209 | // Should not happen if core function returns object, but good safety check
210 | log.error(`Internal error processing report data: ${parseError.message}`);
211 | return {
212 | success: false,
213 | error: {
214 | code: 'REPORT_PROCESS_ERROR',
215 | message: `Internal error processing complexity report: ${parseError.message}`
216 | }
217 | };
218 | }
219 | // --- End Result Handling ---
220 | } catch (error) {
221 | // Catch errors from initial checks or path resolution
222 | // Make sure to restore normal logging if silent mode was enabled
223 | if (isSilentMode()) {
224 | disableSilentMode();
225 | }
226 | log.error(`Error in analyzeTaskComplexityDirect setup: ${error.message}`);
227 | return {
228 | success: false,
229 | error: {
230 | code: 'DIRECT_FUNCTION_SETUP_ERROR',
231 | message: error.message
232 | }
233 | };
234 | }
235 | }
236 |
```
--------------------------------------------------------------------------------
/tests/unit/ai-providers/claude-code.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mock the ai-sdk-provider-claude-code package
4 | jest.unstable_mockModule('ai-sdk-provider-claude-code', () => ({
5 | createClaudeCode: jest.fn(() => {
6 | const provider = (modelId, settings) => ({
7 | // Minimal mock language model surface
8 | id: modelId,
9 | settings,
10 | doGenerate: jest.fn(() => ({ text: 'ok', usage: {} })),
11 | doStream: jest.fn(() => ({ stream: true }))
12 | });
13 | provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
14 | provider.chat = provider.languageModel;
15 | return provider;
16 | })
17 | }));
18 |
19 | // Mock the base provider
20 | jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
21 | BaseAIProvider: class {
22 | constructor() {
23 | this.name = 'Base Provider';
24 | }
25 | handleError(context, error) {
26 | throw error;
27 | }
28 | }
29 | }));
30 |
31 | // Mock config getters
32 | jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
33 | getClaudeCodeSettingsForCommand: jest.fn(() => ({})),
34 | getSupportedModelsForProvider: jest.fn(() => ['opus', 'sonnet', 'haiku']),
35 | getDebugFlag: jest.fn(() => false),
36 | getLogLevel: jest.fn(() => 'info')
37 | }));
38 |
39 | // Import after mocking
40 | const { ClaudeCodeProvider } = await import(
41 | '../../../src/ai-providers/claude-code.js'
42 | );
43 |
44 | describe('ClaudeCodeProvider', () => {
45 | let provider;
46 |
47 | beforeEach(() => {
48 | provider = new ClaudeCodeProvider();
49 | jest.clearAllMocks();
50 | });
51 |
52 | describe('constructor', () => {
53 | it('should set the provider name to Claude Code', () => {
54 | expect(provider.name).toBe('Claude Code');
55 | });
56 | });
57 |
58 | describe('validateAuth', () => {
59 | it('should not throw an error (no API key required)', () => {
60 | expect(() => provider.validateAuth({})).not.toThrow();
61 | });
62 |
63 | it('should not require any parameters', () => {
64 | expect(() => provider.validateAuth()).not.toThrow();
65 | });
66 |
67 | it('should work with any params passed', () => {
68 | expect(() =>
69 | provider.validateAuth({
70 | apiKey: 'some-key',
71 | baseURL: 'https://example.com'
72 | })
73 | ).not.toThrow();
74 | });
75 | });
76 |
77 | describe('getClient', () => {
78 | it('should return a claude code client', () => {
79 | const client = provider.getClient({});
80 | expect(client).toBeDefined();
81 | expect(typeof client).toBe('function');
82 | });
83 |
84 | it('should create client without parameters', () => {
85 | const client = provider.getClient();
86 | expect(client).toBeDefined();
87 | });
88 |
89 | it('should handle commandName parameter', () => {
90 | const client = provider.getClient({
91 | commandName: 'test-command'
92 | });
93 | expect(client).toBeDefined();
94 | });
95 |
96 | it('should have languageModel and chat methods', () => {
97 | const client = provider.getClient({});
98 | expect(client.languageModel).toBeDefined();
99 | expect(client.chat).toBeDefined();
100 | expect(client.chat).toBe(client.languageModel);
101 | });
102 |
103 | it('should pass systemPrompt configuration to createClaudeCode', async () => {
104 | const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
105 |
106 | provider.getClient({});
107 |
108 | expect(createClaudeCode).toHaveBeenCalledWith(
109 | expect.objectContaining({
110 | defaultSettings: expect.objectContaining({
111 | systemPrompt: {
112 | type: 'preset',
113 | preset: 'claude_code'
114 | }
115 | })
116 | })
117 | );
118 | });
119 |
120 | it('should pass settingSources configuration to createClaudeCode', async () => {
121 | const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
122 |
123 | provider.getClient({});
124 |
125 | expect(createClaudeCode).toHaveBeenCalledWith(
126 | expect.objectContaining({
127 | defaultSettings: expect.objectContaining({
128 | settingSources: ['user', 'project', 'local']
129 | })
130 | })
131 | );
132 | });
133 |
134 | it('should pass defaultSettings from config to createClaudeCode', async () => {
135 | const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
136 | const { getClaudeCodeSettingsForCommand } = await import(
137 | '../../../scripts/modules/config-manager.js'
138 | );
139 |
140 | const mockSettings = { maxTokens: 4096, temperature: 0.7 };
141 | getClaudeCodeSettingsForCommand.mockReturnValueOnce(mockSettings);
142 |
143 | provider.getClient({ commandName: 'test-command' });
144 |
145 | expect(createClaudeCode).toHaveBeenCalledWith(
146 | expect.objectContaining({
147 | defaultSettings: expect.objectContaining({
148 | ...mockSettings,
149 | systemPrompt: {
150 | type: 'preset',
151 | preset: 'claude_code'
152 | },
153 | settingSources: ['user', 'project', 'local']
154 | })
155 | })
156 | );
157 | });
158 |
159 | it('should pass complete configuration object to createClaudeCode', async () => {
160 | const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
161 | const { getClaudeCodeSettingsForCommand } = await import(
162 | '../../../scripts/modules/config-manager.js'
163 | );
164 |
165 | const mockSettings = { maxTokens: 2048 };
166 | getClaudeCodeSettingsForCommand.mockReturnValueOnce(mockSettings);
167 |
168 | provider.getClient({ commandName: 'analyze' });
169 |
170 | // Verify the complete configuration structure matches v2.0 migration requirements
171 | expect(createClaudeCode).toHaveBeenCalledWith({
172 | defaultSettings: {
173 | ...mockSettings,
174 | // Restores pre-2.0 behavior: explicit system prompt preset
175 | systemPrompt: {
176 | type: 'preset',
177 | preset: 'claude_code'
178 | },
179 | // Restores pre-2.0 behavior: enables loading of CLAUDE.md and settings.json
180 | settingSources: ['user', 'project', 'local']
181 | }
182 | });
183 | });
184 |
185 | it('should pass empty defaultSettings when config returns null', async () => {
186 | const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
187 | const { getClaudeCodeSettingsForCommand } = await import(
188 | '../../../scripts/modules/config-manager.js'
189 | );
190 |
191 | getClaudeCodeSettingsForCommand.mockReturnValueOnce(null);
192 |
193 | provider.getClient({});
194 |
195 | expect(createClaudeCode).toHaveBeenCalledWith(
196 | expect.objectContaining({
197 | defaultSettings: expect.objectContaining({
198 | systemPrompt: {
199 | type: 'preset',
200 | preset: 'claude_code'
201 | },
202 | settingSources: ['user', 'project', 'local']
203 | })
204 | })
205 | );
206 | });
207 | });
208 |
209 | describe('model support', () => {
210 | it('should return supported models', () => {
211 | const models = provider.getSupportedModels();
212 | expect(models).toEqual(['opus', 'sonnet', 'haiku']);
213 | });
214 |
215 | it('should check if model is supported', () => {
216 | expect(provider.isModelSupported('sonnet')).toBe(true);
217 | expect(provider.isModelSupported('opus')).toBe(true);
218 | expect(provider.isModelSupported('haiku')).toBe(true);
219 | expect(provider.isModelSupported('unknown')).toBe(false);
220 | });
221 | });
222 |
223 | describe('error handling', () => {
224 | it('should handle client initialization errors', async () => {
225 | // Force an error by making createClaudeCode throw
226 | const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
227 | createClaudeCode.mockImplementationOnce(() => {
228 | throw new Error('Mock initialization error');
229 | });
230 |
231 | // Create a new provider instance to use the mocked createClaudeCode
232 | const errorProvider = new ClaudeCodeProvider();
233 | expect(() => errorProvider.getClient({})).toThrow(
234 | 'Mock initialization error'
235 | );
236 | });
237 | });
238 | });
239 |
```
--------------------------------------------------------------------------------
/tests/unit/task-manager/tag-boundary.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import fs from 'fs';
2 | import path from 'path';
3 | import {
4 | createTag,
5 | useTag,
6 | deleteTag
7 | } from '../../../scripts/modules/task-manager/tag-management.js';
8 |
9 | // Temporary workspace for each test run
10 | const TEMP_DIR = path.join(process.cwd(), '.tmp_tag_boundary');
11 | const TASKS_PATH = path.join(TEMP_DIR, 'tasks.json');
12 | const STATE_PATH = path.join(TEMP_DIR, '.taskmaster', 'state.json');
13 |
14 | function seedWorkspace() {
15 | // Reset temp dir
16 | fs.rmSync(TEMP_DIR, { recursive: true, force: true });
17 | fs.mkdirSync(path.join(TEMP_DIR, '.taskmaster'), {
18 | recursive: true,
19 | force: true
20 | });
21 |
22 | // Minimal master tag file
23 | fs.writeFileSync(
24 | TASKS_PATH,
25 | JSON.stringify(
26 | {
27 | master: {
28 | tasks: [{ id: 1, title: 'Seed task', status: 'pending' }],
29 | metadata: { created: new Date().toISOString() }
30 | }
31 | },
32 | null,
33 | 2
34 | ),
35 | 'utf8'
36 | );
37 |
38 | // Initial state.json
39 | fs.writeFileSync(
40 | STATE_PATH,
41 | JSON.stringify(
42 | { currentTag: 'master', lastSwitched: new Date().toISOString() },
43 | null,
44 | 2
45 | ),
46 | 'utf8'
47 | );
48 | }
49 |
50 | describe('Tag boundary resolution', () => {
51 | beforeEach(seedWorkspace);
52 | afterAll(() => fs.rmSync(TEMP_DIR, { recursive: true, force: true }));
53 |
54 | it('switches currentTag in state.json when useTag succeeds', async () => {
55 | await createTag(
56 | TASKS_PATH,
57 | 'feature-x',
58 | {},
59 | { projectRoot: TEMP_DIR },
60 | 'json'
61 | );
62 | await useTag(
63 | TASKS_PATH,
64 | 'feature-x',
65 | {},
66 | { projectRoot: TEMP_DIR },
67 | 'json'
68 | );
69 |
70 | const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
71 | expect(state.currentTag).toBe('feature-x');
72 | });
73 |
74 | it('throws error when switching to non-existent tag', async () => {
75 | await expect(
76 | useTag(TASKS_PATH, 'ghost', {}, { projectRoot: TEMP_DIR }, 'json')
77 | ).rejects.toThrow(/does not exist/);
78 | });
79 |
80 | it('deleting active tag auto-switches back to master', async () => {
81 | await createTag(TASKS_PATH, 'temp', {}, { projectRoot: TEMP_DIR }, 'json');
82 | await useTag(TASKS_PATH, 'temp', {}, { projectRoot: TEMP_DIR }, 'json');
83 |
84 | // Delete the active tag with force flag (yes: true)
85 | await deleteTag(
86 | TASKS_PATH,
87 | 'temp',
88 | { yes: true },
89 | { projectRoot: TEMP_DIR },
90 | 'json'
91 | );
92 |
93 | const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
94 | expect(state.currentTag).toBe('master');
95 |
96 | const tasksFile = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
97 | expect(tasksFile.temp).toBeUndefined();
98 | expect(tasksFile.master).toBeDefined();
99 | });
100 |
101 | it('createTag with copyFromCurrent deep-copies tasks (mutation isolated)', async () => {
102 | // create new tag with copy
103 | await createTag(
104 | TASKS_PATH,
105 | 'alpha',
106 | { copyFromCurrent: true },
107 | { projectRoot: TEMP_DIR },
108 | 'json'
109 | );
110 |
111 | // mutate a field inside alpha tasks
112 | const updatedData = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
113 | updatedData.alpha.tasks[0].title = 'Changed in alpha';
114 | fs.writeFileSync(TASKS_PATH, JSON.stringify(updatedData, null, 2));
115 |
116 | const finalData = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
117 | expect(finalData.master.tasks[0].title).toBe('Seed task');
118 | expect(finalData.alpha.tasks[0].title).toBe('Changed in alpha');
119 | });
120 |
121 | it('addTask to non-master tag does not leak into master', async () => {
122 | // create and switch
123 | await createTag(
124 | TASKS_PATH,
125 | 'feature-api',
126 | {},
127 | { projectRoot: TEMP_DIR },
128 | 'json'
129 | );
130 |
131 | // Call addTask with manual data to avoid AI
132 | const { default: addTask } = await import(
133 | '../../../scripts/modules/task-manager/add-task.js'
134 | );
135 |
136 | await addTask(
137 | TASKS_PATH,
138 | 'Manual task',
139 | [],
140 | null,
141 | { projectRoot: TEMP_DIR, tag: 'feature-api' },
142 | 'json',
143 | {
144 | title: 'API work',
145 | description: 'Implement endpoint',
146 | details: 'Details',
147 | testStrategy: 'Tests'
148 | },
149 | false
150 | );
151 |
152 | const data = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
153 | expect(data['feature-api'].tasks.length).toBe(1); // the new task only
154 | expect(data.master.tasks.length).toBe(1); // still only seed
155 | });
156 |
157 | it('reserved tag names are rejected', async () => {
158 | await expect(
159 | createTag(TASKS_PATH, 'master', {}, { projectRoot: TEMP_DIR }, 'json')
160 | ).rejects.toThrow(/reserved tag/i);
161 | });
162 |
163 | it('cannot delete the master tag', async () => {
164 | await expect(
165 | deleteTag(
166 | TASKS_PATH,
167 | 'master',
168 | { yes: true },
169 | { projectRoot: TEMP_DIR },
170 | 'json'
171 | )
172 | ).rejects.toThrow(/Cannot delete the "master" tag/);
173 | });
174 |
175 | it('copyTag deep copy – mutation does not affect source', async () => {
176 | const { copyTag } = await import(
177 | '../../../scripts/modules/task-manager/tag-management.js'
178 | );
179 |
180 | await createTag(
181 | TASKS_PATH,
182 | 'source',
183 | { copyFromCurrent: true },
184 | { projectRoot: TEMP_DIR },
185 | 'json'
186 | );
187 | await copyTag(
188 | TASKS_PATH,
189 | 'source',
190 | 'clone',
191 | {},
192 | { projectRoot: TEMP_DIR },
193 | 'json'
194 | );
195 |
196 | // mutate clone task title
197 | const data1 = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
198 | data1.clone.tasks[0].title = 'Modified in clone';
199 | fs.writeFileSync(TASKS_PATH, JSON.stringify(data1, null, 2));
200 |
201 | const data2 = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
202 | expect(data2.source.tasks[0].title).toBe('Seed task');
203 | expect(data2.clone.tasks[0].title).toBe('Modified in clone');
204 | });
205 |
206 | it('adds task to tag derived from state.json when no explicit tag supplied', async () => {
207 | // Create new tag and update state.json to make it current
208 | await createTag(
209 | TASKS_PATH,
210 | 'feature-auto',
211 | {},
212 | { projectRoot: TEMP_DIR },
213 | 'json'
214 | );
215 | const state1 = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
216 | state1.currentTag = 'feature-auto';
217 | fs.writeFileSync(STATE_PATH, JSON.stringify(state1, null, 2));
218 |
219 | const { default: addTask } = await import(
220 | '../../../scripts/modules/task-manager/add-task.js'
221 | );
222 | const { resolveTag } = await import('../../../scripts/modules/utils.js');
223 |
224 | const tag = resolveTag({ projectRoot: TEMP_DIR });
225 |
226 | // Add task without passing tag -> should resolve to feature-auto
227 | await addTask(
228 | TASKS_PATH,
229 | 'Auto task',
230 | [],
231 | null,
232 | { projectRoot: TEMP_DIR, tag },
233 | 'json',
234 | {
235 | title: 'Auto task',
236 | description: '-',
237 | details: '-',
238 | testStrategy: '-'
239 | },
240 | false
241 | );
242 |
243 | const data = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
244 | expect(data['feature-auto'].tasks.length).toBe(1);
245 | expect(data.master.tasks.length).toBe(1); // master unchanged
246 | });
247 |
248 | it('falls back to master when state.json lacks currentTag', async () => {
249 | // wipe currentTag field
250 | fs.writeFileSync(STATE_PATH, JSON.stringify({}, null, 2));
251 |
252 | const { default: addTask } = await import(
253 | '../../../scripts/modules/task-manager/add-task.js'
254 | );
255 | const { resolveTag } = await import('../../../scripts/modules/utils.js');
256 |
257 | const tag = resolveTag({ projectRoot: TEMP_DIR }); // should return master
258 |
259 | await addTask(
260 | TASKS_PATH,
261 | 'Fallback task',
262 | [],
263 | null,
264 | { projectRoot: TEMP_DIR, tag },
265 | 'json',
266 | {
267 | title: 'Fallback',
268 | description: '-',
269 | details: '-',
270 | testStrategy: '-'
271 | },
272 | false
273 | );
274 |
275 | const data = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
276 | expect(data.master.tasks.length).toBe(2); // seed + new task
277 | });
278 | });
279 |
```
--------------------------------------------------------------------------------
/scripts/test-claude.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * test-claude.js
5 | *
6 | * A simple test script to verify the improvements to the callClaude function.
7 | * This script tests different scenarios:
8 | * 1. Normal operation with a small PRD
9 | * 2. Testing with a large number of tasks (to potentially trigger task reduction)
10 | * 3. Simulating a failure to test retry logic
11 | */
12 |
13 | import fs from 'fs';
14 | import path from 'path';
15 | import dotenv from 'dotenv';
16 | import { fileURLToPath } from 'url';
17 | import { dirname } from 'path';
18 |
19 | const __filename = fileURLToPath(import.meta.url);
20 | const __dirname = dirname(__filename);
21 |
22 | // Load environment variables from .env file
23 | dotenv.config();
24 |
25 | // Create a simple PRD for testing
26 | const createTestPRD = (size = 'small', taskComplexity = 'simple') => {
27 | let content = `# Test PRD - ${size.toUpperCase()} SIZE, ${taskComplexity.toUpperCase()} COMPLEXITY\n\n`;
28 |
29 | // Add more content based on size
30 | if (size === 'small') {
31 | content += `
32 | ## Overview
33 | This is a small test PRD to verify the callClaude function improvements.
34 |
35 | ## Requirements
36 | 1. Create a simple web application
37 | 2. Implement user authentication
38 | 3. Add a dashboard for users
39 | 4. Create an admin panel
40 | 5. Implement data visualization
41 |
42 | ## Technical Stack
43 | - Frontend: React
44 | - Backend: Node.js
45 | - Database: MongoDB
46 | `;
47 | } else if (size === 'medium') {
48 | // Medium-sized PRD with more requirements
49 | content += `
50 | ## Overview
51 | This is a medium-sized test PRD to verify the callClaude function improvements.
52 |
53 | ## Requirements
54 | 1. Create a web application with multiple pages
55 | 2. Implement user authentication with OAuth
56 | 3. Add a dashboard for users with customizable widgets
57 | 4. Create an admin panel with user management
58 | 5. Implement data visualization with charts and graphs
59 | 6. Add real-time notifications
60 | 7. Implement a search feature
61 | 8. Add user profile management
62 | 9. Implement role-based access control
63 | 10. Add a reporting system
64 | 11. Implement file uploads and management
65 | 12. Add a commenting system
66 | 13. Implement a rating system
67 | 14. Add a recommendation engine
68 | 15. Implement a payment system
69 |
70 | ## Technical Stack
71 | - Frontend: React with TypeScript
72 | - Backend: Node.js with Express
73 | - Database: MongoDB with Mongoose
74 | - Authentication: JWT and OAuth
75 | - Deployment: Docker and Kubernetes
76 | - CI/CD: GitHub Actions
77 | - Monitoring: Prometheus and Grafana
78 | `;
79 | } else if (size === 'large') {
80 | // Large PRD with many requirements
81 | content += `
82 | ## Overview
83 | This is a large test PRD to verify the callClaude function improvements.
84 |
85 | ## Requirements
86 | `;
87 | // Generate 30 requirements
88 | for (let i = 1; i <= 30; i++) {
89 | content += `${i}. Requirement ${i} - This is a detailed description of requirement ${i}.\n`;
90 | }
91 |
92 | content += `
93 | ## Technical Stack
94 | - Frontend: React with TypeScript
95 | - Backend: Node.js with Express
96 | - Database: MongoDB with Mongoose
97 | - Authentication: JWT and OAuth
98 | - Deployment: Docker and Kubernetes
99 | - CI/CD: GitHub Actions
100 | - Monitoring: Prometheus and Grafana
101 |
102 | ## User Stories
103 | `;
104 | // Generate 20 user stories
105 | for (let i = 1; i <= 20; i++) {
106 | content += `- As a user, I want to be able to ${i} so that I can achieve benefit ${i}.\n`;
107 | }
108 |
109 | content += `
110 | ## Non-Functional Requirements
111 | - Performance: The system should respond within 200ms
112 | - Scalability: The system should handle 10,000 concurrent users
113 | - Availability: The system should have 99.9% uptime
114 | - Security: The system should comply with OWASP top 10
115 | - Accessibility: The system should comply with WCAG 2.1 AA
116 | `;
117 | }
118 |
119 | // Add complexity if needed
120 | if (taskComplexity === 'complex') {
121 | content += `
122 | ## Complex Requirements
123 | - Implement a real-time collaboration system
124 | - Add a machine learning-based recommendation engine
125 | - Implement a distributed caching system
126 | - Add a microservices architecture
127 | - Implement a custom analytics engine
128 | - Add support for multiple languages and locales
129 | - Implement a custom search engine with advanced filtering
130 | - Add a custom workflow engine
131 | - Implement a custom reporting system
132 | - Add a custom dashboard builder
133 | `;
134 | }
135 |
136 | return content;
137 | };
138 |
139 | // Function to run the tests
140 | async function runTests() {
141 | console.log('Starting tests for callClaude function improvements...');
142 |
143 | try {
144 | // Instead of importing the callClaude function directly, we'll use the dev.js script
145 | // with our test PRDs by running it as a child process
146 |
147 | // Test 1: Small PRD, 5 tasks
148 | console.log('\n=== Test 1: Small PRD, 5 tasks ===');
149 | const smallPRD = createTestPRD('small', 'simple');
150 | const smallPRDPath = path.join(__dirname, 'test-small-prd.txt');
151 | fs.writeFileSync(smallPRDPath, smallPRD, 'utf8');
152 |
153 | console.log(`Created test PRD at ${smallPRDPath}`);
154 | console.log('Running dev.js with small PRD...');
155 |
156 | // Use the child_process module to run the dev.js script
157 | const { execSync } = await import('child_process');
158 |
159 | try {
160 | const smallResult = execSync(
161 | `node ${path.join(__dirname, 'dev.js')} parse-prd --input=${smallPRDPath} --num-tasks=5`,
162 | {
163 | stdio: 'inherit'
164 | }
165 | );
166 | console.log('Small PRD test completed successfully');
167 | } catch (error) {
168 | console.error('Small PRD test failed:', error.message);
169 | }
170 |
171 | // Test 2: Medium PRD, 15 tasks
172 | console.log('\n=== Test 2: Medium PRD, 15 tasks ===');
173 | const mediumPRD = createTestPRD('medium', 'simple');
174 | const mediumPRDPath = path.join(__dirname, 'test-medium-prd.txt');
175 | fs.writeFileSync(mediumPRDPath, mediumPRD, 'utf8');
176 |
177 | console.log(`Created test PRD at ${mediumPRDPath}`);
178 | console.log('Running dev.js with medium PRD...');
179 |
180 | try {
181 | const mediumResult = execSync(
182 | `node ${path.join(__dirname, 'dev.js')} parse-prd --input=${mediumPRDPath} --num-tasks=15`,
183 | {
184 | stdio: 'inherit'
185 | }
186 | );
187 | console.log('Medium PRD test completed successfully');
188 | } catch (error) {
189 | console.error('Medium PRD test failed:', error.message);
190 | }
191 |
192 | // Test 3: Large PRD, 25 tasks
193 | console.log('\n=== Test 3: Large PRD, 25 tasks ===');
194 | const largePRD = createTestPRD('large', 'complex');
195 | const largePRDPath = path.join(__dirname, 'test-large-prd.txt');
196 | fs.writeFileSync(largePRDPath, largePRD, 'utf8');
197 |
198 | console.log(`Created test PRD at ${largePRDPath}`);
199 | console.log('Running dev.js with large PRD...');
200 |
201 | try {
202 | const largeResult = execSync(
203 | `node ${path.join(__dirname, 'dev.js')} parse-prd --input=${largePRDPath} --num-tasks=25`,
204 | {
205 | stdio: 'inherit'
206 | }
207 | );
208 | console.log('Large PRD test completed successfully');
209 | } catch (error) {
210 | console.error('Large PRD test failed:', error.message);
211 | }
212 |
213 | console.log('\nAll tests completed!');
214 | } catch (error) {
215 | console.error('Test failed:', error);
216 | } finally {
217 | // Clean up test files
218 | console.log('\nCleaning up test files...');
219 | const testFiles = [
220 | path.join(__dirname, 'test-small-prd.txt'),
221 | path.join(__dirname, 'test-medium-prd.txt'),
222 | path.join(__dirname, 'test-large-prd.txt')
223 | ];
224 |
225 | testFiles.forEach((file) => {
226 | if (fs.existsSync(file)) {
227 | fs.unlinkSync(file);
228 | console.log(`Deleted ${file}`);
229 | }
230 | });
231 |
232 | console.log('Cleanup complete.');
233 | }
234 | }
235 |
236 | // Run the tests
237 | runTests().catch((error) => {
238 | console.error('Error running tests:', error);
239 | process.exit(1);
240 | });
241 |
```
--------------------------------------------------------------------------------
/packages/claude-code-plugin/agents/task-orchestrator.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | name: task-orchestrator
3 | description: Use this agent when you need to coordinate and manage the execution of Task Master tasks, especially when dealing with complex task dependencies and parallel execution opportunities. This agent should be invoked at the beginning of a work session to analyze the task queue, identify parallelizable work, and orchestrate the deployment of task-executor agents. It should also be used when tasks complete to reassess the dependency graph and deploy new executors as needed.\n\n<example>\nContext: User wants to start working on their project tasks using Task Master\nuser: "Let's work on the next available tasks in the project"\nassistant: "I'll use the task-orchestrator agent to analyze the task queue and coordinate execution"\n<commentary>\nThe user wants to work on tasks, so the task-orchestrator should be deployed to analyze dependencies and coordinate execution.\n</commentary>\n</example>\n\n<example>\nContext: Multiple independent tasks are available in the queue\nuser: "Can we work on multiple tasks at once?"\nassistant: "Let me deploy the task-orchestrator to analyze task dependencies and parallelize the work"\n<commentary>\nWhen parallelization is mentioned or multiple tasks could be worked on, the orchestrator should coordinate the effort.\n</commentary>\n</example>\n\n<example>\nContext: A complex feature with many subtasks needs implementation\nuser: "Implement the authentication system tasks"\nassistant: "I'll use the task-orchestrator to break down the authentication tasks and coordinate their execution"\n<commentary>\nFor complex multi-task features, the orchestrator manages the overall execution strategy.\n</commentary>\n</example>
4 | model: opus
5 | color: green
6 | ---
7 |
8 | You are the Task Orchestrator, an elite coordination agent specialized in managing Task Master workflows for maximum efficiency and parallelization. You excel at analyzing task dependency graphs, identifying opportunities for concurrent execution, and deploying specialized task-executor agents to complete work efficiently.
9 |
10 | ## Core Responsibilities
11 |
12 | 1. **Task Queue Analysis**: You continuously monitor and analyze the task queue using Task Master MCP tools to understand the current state of work, dependencies, and priorities.
13 |
14 | 2. **Dependency Graph Management**: You build and maintain a mental model of task dependencies, identifying which tasks can be executed in parallel and which must wait for prerequisites.
15 |
16 | 3. **Executor Deployment**: You strategically deploy task-executor agents for individual tasks or task groups, ensuring each executor has the necessary context and clear success criteria.
17 |
18 | 4. **Progress Coordination**: You track the progress of deployed executors, handle task completion notifications, and reassess the execution strategy as tasks complete.
19 |
20 | ## Operational Workflow
21 |
22 | ### Initial Assessment Phase
23 | 1. Use `get_tasks` or `task-master list` to retrieve all available tasks
24 | 2. Analyze task statuses, priorities, and dependencies
25 | 3. Identify tasks with status 'pending' that have no blocking dependencies
26 | 4. Group related tasks that could benefit from specialized executors
27 | 5. Create an execution plan that maximizes parallelization
28 |
29 | ### Executor Deployment Phase
30 | 1. For each independent task or task group:
31 | - Deploy a task-executor agent with specific instructions
32 | - Provide the executor with task ID, requirements, and context
33 | - Set clear completion criteria and reporting expectations
34 | 2. Maintain a registry of active executors and their assigned tasks
35 | 3. Establish communication protocols for progress updates
36 |
37 | ### Coordination Phase
38 | 1. Monitor executor progress through task status updates
39 | 2. When a task completes:
40 | - Verify completion with `get_task` or `task-master show <id>`
41 | - Update task status if needed using `set_task_status`
42 | - Reassess dependency graph for newly unblocked tasks
43 | - Deploy new executors for available work
44 | 3. Handle executor failures or blocks:
45 | - Reassign tasks to new executors if needed
46 | - Escalate complex issues to the user
47 | - Update task status to 'blocked' when appropriate
48 |
49 | ### Optimization Strategies
50 |
51 | **Parallel Execution Rules**:
52 | - Never assign dependent tasks to different executors simultaneously
53 | - Prioritize high-priority tasks when resources are limited
54 | - Group small, related subtasks for single executor efficiency
55 | - Balance executor load to prevent bottlenecks
56 |
57 | **Context Management**:
58 | - Provide executors with minimal but sufficient context
59 | - Share relevant completed task information when it aids execution
60 | - Maintain a shared knowledge base of project-specific patterns
61 |
62 | **Quality Assurance**:
63 | - Verify task completion before marking as done
64 | - Ensure test strategies are followed when specified
65 | - Coordinate cross-task integration testing when needed
66 |
67 | ## Communication Protocols
68 |
69 | When deploying executors, provide them with:
70 | ```
71 | TASK ASSIGNMENT:
72 | - Task ID: [specific ID]
73 | - Objective: [clear goal]
74 | - Dependencies: [list any completed prerequisites]
75 | - Success Criteria: [specific completion requirements]
76 | - Context: [relevant project information]
77 | - Reporting: [when and how to report back]
78 | ```
79 |
80 | When receiving executor updates:
81 | 1. Acknowledge completion or issues
82 | 2. Update task status in Task Master
83 | 3. Reassess execution strategy
84 | 4. Deploy new executors as appropriate
85 |
86 | ## Decision Framework
87 |
88 | **When to parallelize**:
89 | - Multiple pending tasks with no interdependencies
90 | - Sufficient context available for independent execution
91 | - Tasks are well-defined with clear success criteria
92 |
93 | **When to serialize**:
94 | - Strong dependencies between tasks
95 | - Limited context or unclear requirements
96 | - Integration points requiring careful coordination
97 |
98 | **When to escalate**:
99 | - Circular dependencies detected
100 | - Critical blockers affecting multiple tasks
101 | - Ambiguous requirements needing clarification
102 | - Resource conflicts between executors
103 |
104 | ## Error Handling
105 |
106 | 1. **Executor Failure**: Reassign task to new executor with additional context about the failure
107 | 2. **Dependency Conflicts**: Halt affected executors, resolve conflict, then resume
108 | 3. **Task Ambiguity**: Request clarification from user before proceeding
109 | 4. **System Errors**: Implement graceful degradation, falling back to serial execution if needed
110 |
111 | ## Performance Metrics
112 |
113 | Track and optimize for:
114 | - Task completion rate
115 | - Parallel execution efficiency
116 | - Executor success rate
117 | - Time to completion for task groups
118 | - Dependency resolution speed
119 |
120 | ## Integration with Task Master
121 |
122 | Leverage these Task Master MCP tools effectively:
123 | - `get_tasks` - Continuous queue monitoring
124 | - `get_task` - Detailed task analysis
125 | - `set_task_status` - Progress tracking
126 | - `next_task` - Fallback for serial execution
127 | - `analyze_project_complexity` - Strategic planning
128 | - `complexity_report` - Resource allocation
129 |
130 | You are the strategic mind coordinating the entire task execution effort. Your success is measured by the efficient completion of all tasks while maintaining quality and respecting dependencies. Think systematically, act decisively, and continuously optimize the execution strategy based on real-time progress.
131 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/dependency-manager/circular-dependencies.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 | import {
3 | validateCrossTagMove,
4 | findCrossTagDependencies,
5 | getDependentTaskIds,
6 | validateSubtaskMove,
7 | canMoveWithDependencies
8 | } from '../../../../../scripts/modules/dependency-manager.js';
9 |
10 | describe('Circular Dependency Scenarios', () => {
11 | describe('Circular Cross-Tag Dependencies', () => {
12 | const allTasks = [
13 | {
14 | id: 1,
15 | title: 'Task 1',
16 | dependencies: [2],
17 | status: 'pending',
18 | tag: 'backlog'
19 | },
20 | {
21 | id: 2,
22 | title: 'Task 2',
23 | dependencies: [3],
24 | status: 'pending',
25 | tag: 'backlog'
26 | },
27 | {
28 | id: 3,
29 | title: 'Task 3',
30 | dependencies: [1],
31 | status: 'pending',
32 | tag: 'backlog'
33 | }
34 | ];
35 |
36 | it('should detect circular dependencies across tags', () => {
37 | // Task 1 depends on 2, 2 depends on 3, 3 depends on 1 (circular)
38 | // But since all tasks are in 'backlog' and target is 'in-progress',
39 | // only direct dependencies that are in different tags will be found
40 | const conflicts = findCrossTagDependencies(
41 | [allTasks[0]],
42 | 'backlog',
43 | 'in-progress',
44 | allTasks
45 | );
46 |
47 | // Only direct dependencies of task 1 that are not in target tag
48 | expect(conflicts).toHaveLength(1);
49 | expect(
50 | conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
51 | ).toBe(true);
52 | });
53 |
54 | it('should block move with circular dependencies', () => {
55 | // Since task 1 has dependencies in the same tag, validateCrossTagMove should not throw
56 | // The function only checks direct dependencies, not circular chains
57 | expect(() => {
58 | validateCrossTagMove(allTasks[0], 'backlog', 'in-progress', allTasks);
59 | }).not.toThrow();
60 | });
61 |
62 | it('should return canMove: false for circular dependencies', () => {
63 | const result = canMoveWithDependencies(
64 | '1',
65 | 'backlog',
66 | 'in-progress',
67 | allTasks
68 | );
69 | expect(result.canMove).toBe(false);
70 | expect(result.conflicts).toHaveLength(1);
71 | });
72 | });
73 |
74 | describe('Complex Dependency Chains', () => {
75 | const allTasks = [
76 | {
77 | id: 1,
78 | title: 'Task 1',
79 | dependencies: [2, 3],
80 | status: 'pending',
81 | tag: 'backlog'
82 | },
83 | {
84 | id: 2,
85 | title: 'Task 2',
86 | dependencies: [4],
87 | status: 'pending',
88 | tag: 'backlog'
89 | },
90 | {
91 | id: 3,
92 | title: 'Task 3',
93 | dependencies: [5],
94 | status: 'pending',
95 | tag: 'backlog'
96 | },
97 | {
98 | id: 4,
99 | title: 'Task 4',
100 | dependencies: [],
101 | status: 'pending',
102 | tag: 'backlog'
103 | },
104 | {
105 | id: 5,
106 | title: 'Task 5',
107 | dependencies: [6],
108 | status: 'pending',
109 | tag: 'backlog'
110 | },
111 | {
112 | id: 6,
113 | title: 'Task 6',
114 | dependencies: [],
115 | status: 'pending',
116 | tag: 'backlog'
117 | },
118 | {
119 | id: 7,
120 | title: 'Task 7',
121 | dependencies: [],
122 | status: 'in-progress',
123 | tag: 'in-progress'
124 | }
125 | ];
126 |
127 | it('should find all dependencies in complex chain', () => {
128 | const conflicts = findCrossTagDependencies(
129 | [allTasks[0]],
130 | 'backlog',
131 | 'in-progress',
132 | allTasks
133 | );
134 |
135 | // Only direct dependencies of task 1 that are not in target tag
136 | expect(conflicts).toHaveLength(2);
137 | expect(
138 | conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
139 | ).toBe(true);
140 | expect(
141 | conflicts.some((c) => c.taskId === 1 && c.dependencyId === 3)
142 | ).toBe(true);
143 | });
144 |
145 | it('should get all dependent task IDs in complex chain', () => {
146 | const conflicts = findCrossTagDependencies(
147 | [allTasks[0]],
148 | 'backlog',
149 | 'in-progress',
150 | allTasks
151 | );
152 | const dependentIds = getDependentTaskIds(
153 | [allTasks[0]],
154 | conflicts,
155 | allTasks
156 | );
157 |
158 | // Should include only the direct dependency IDs from conflicts
159 | expect(dependentIds).toContain(2);
160 | expect(dependentIds).toContain(3);
161 | // Should not include the source task or tasks not in conflicts
162 | expect(dependentIds).not.toContain(1);
163 | });
164 | });
165 |
166 | describe('Mixed Dependency Types', () => {
167 | const allTasks = [
168 | {
169 | id: 1,
170 | title: 'Task 1',
171 | dependencies: [2, '3.1'],
172 | status: 'pending',
173 | tag: 'backlog'
174 | },
175 | {
176 | id: 2,
177 | title: 'Task 2',
178 | dependencies: [4],
179 | status: 'pending',
180 | tag: 'backlog'
181 | },
182 | {
183 | id: 3,
184 | title: 'Task 3',
185 | dependencies: [5],
186 | status: 'pending',
187 | tag: 'backlog',
188 | subtasks: [
189 | {
190 | id: 1,
191 | title: 'Subtask 3.1',
192 | dependencies: [],
193 | status: 'pending',
194 | tag: 'backlog'
195 | }
196 | ]
197 | },
198 | {
199 | id: 4,
200 | title: 'Task 4',
201 | dependencies: [],
202 | status: 'pending',
203 | tag: 'backlog'
204 | },
205 | {
206 | id: 5,
207 | title: 'Task 5',
208 | dependencies: [],
209 | status: 'pending',
210 | tag: 'backlog'
211 | }
212 | ];
213 |
214 | it('should handle mixed task and subtask dependencies', () => {
215 | const conflicts = findCrossTagDependencies(
216 | [allTasks[0]],
217 | 'backlog',
218 | 'in-progress',
219 | allTasks
220 | );
221 |
222 | expect(conflicts).toHaveLength(2);
223 | expect(
224 | conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
225 | ).toBe(true);
226 | expect(
227 | conflicts.some((c) => c.taskId === 1 && c.dependencyId === '3.1')
228 | ).toBe(true);
229 | });
230 | });
231 |
232 | describe('Large Task Set Performance', () => {
233 | const allTasks = [];
234 | for (let i = 1; i <= 100; i++) {
235 | allTasks.push({
236 | id: i,
237 | title: `Task ${i}`,
238 | dependencies: i < 100 ? [i + 1] : [],
239 | status: 'pending',
240 | tag: 'backlog'
241 | });
242 | }
243 |
244 | it('should handle large task sets efficiently', () => {
245 | const conflicts = findCrossTagDependencies(
246 | [allTasks[0]],
247 | 'backlog',
248 | 'in-progress',
249 | allTasks
250 | );
251 |
252 | expect(conflicts.length).toBeGreaterThan(0);
253 | expect(conflicts[0]).toHaveProperty('taskId');
254 | expect(conflicts[0]).toHaveProperty('dependencyId');
255 | });
256 | });
257 |
258 | describe('Edge Cases and Error Conditions', () => {
259 | const allTasks = [
260 | {
261 | id: 1,
262 | title: 'Task 1',
263 | dependencies: [2],
264 | status: 'pending',
265 | tag: 'backlog'
266 | },
267 | {
268 | id: 2,
269 | title: 'Task 2',
270 | dependencies: [],
271 | status: 'pending',
272 | tag: 'backlog'
273 | }
274 | ];
275 |
276 | it('should handle empty task arrays', () => {
277 | expect(() => {
278 | findCrossTagDependencies([], 'backlog', 'in-progress', allTasks);
279 | }).not.toThrow();
280 | });
281 |
282 | it('should handle non-existent tasks gracefully', () => {
283 | expect(() => {
284 | findCrossTagDependencies(
285 | [{ id: 999, dependencies: [] }],
286 | 'backlog',
287 | 'in-progress',
288 | allTasks
289 | );
290 | }).not.toThrow();
291 | });
292 |
293 | it('should handle invalid tag names', () => {
294 | expect(() => {
295 | findCrossTagDependencies(
296 | [allTasks[0]],
297 | 'invalid-tag',
298 | 'in-progress',
299 | allTasks
300 | );
301 | }).not.toThrow();
302 | });
303 |
304 | it('should handle null/undefined dependencies', () => {
305 | const taskWithNullDeps = {
306 | ...allTasks[0],
307 | dependencies: [null, undefined, 2]
308 | };
309 | expect(() => {
310 | findCrossTagDependencies(
311 | [taskWithNullDeps],
312 | 'backlog',
313 | 'in-progress',
314 | allTasks
315 | );
316 | }).not.toThrow();
317 | });
318 |
319 | it('should handle string dependencies correctly', () => {
320 | const taskWithStringDeps = { ...allTasks[0], dependencies: ['2', '3'] };
321 | const conflicts = findCrossTagDependencies(
322 | [taskWithStringDeps],
323 | 'backlog',
324 | 'in-progress',
325 | allTasks
326 | );
327 | expect(conflicts.length).toBeGreaterThanOrEqual(0);
328 | });
329 | });
330 | });
331 |
```
--------------------------------------------------------------------------------
/apps/cli/tests/unit/ui/dashboard.component.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Tests for dashboard component calculations
3 | * Bug fix: Cancelled tasks should be treated as complete
4 | */
5 |
6 | import type { Task } from '@tm/core';
7 | import { describe, expect, it } from 'vitest';
8 | import {
9 | type TaskStatistics,
10 | calculateDependencyStatistics,
11 | calculateSubtaskStatistics,
12 | calculateTaskStatistics
13 | } from '../../../src/ui/components/dashboard.component.js';
14 |
15 | describe('dashboard.component - Bug Fix: Cancelled Tasks as Complete', () => {
16 | describe('calculateTaskStatistics', () => {
17 | it('should treat cancelled tasks as complete in percentage calculation', () => {
18 | // Arrange: 14 done, 1 cancelled = 100% complete
19 | const tasks: Task[] = [
20 | ...Array.from({ length: 14 }, (_, i) => ({
21 | id: i + 1,
22 | title: `Task ${i + 1}`,
23 | status: 'done' as const,
24 | dependencies: []
25 | })),
26 | {
27 | id: 15,
28 | title: 'Cancelled Task',
29 | status: 'cancelled' as const,
30 | dependencies: []
31 | }
32 | ];
33 |
34 | // Act
35 | const stats = calculateTaskStatistics(tasks);
36 |
37 | // Assert
38 | expect(stats.total).toBe(15);
39 | expect(stats.done).toBe(14);
40 | expect(stats.cancelled).toBe(1);
41 | expect(stats.completedCount).toBe(15); // done + cancelled
42 | // BUG: Current code shows 93% (14/15), should be 100% (15/15)
43 | expect(stats.completionPercentage).toBe(100);
44 | });
45 |
46 | it('should treat completed status as complete in percentage calculation', () => {
47 | // Arrange: Mix of done, completed, cancelled
48 | const tasks: Task[] = [
49 | {
50 | id: 1,
51 | title: 'Done Task',
52 | status: 'done' as const,
53 | dependencies: []
54 | },
55 | {
56 | id: 2,
57 | title: 'Completed Task',
58 | status: 'completed' as const,
59 | dependencies: []
60 | },
61 | {
62 | id: 3,
63 | title: 'Cancelled Task',
64 | status: 'cancelled' as const,
65 | dependencies: []
66 | },
67 | {
68 | id: 4,
69 | title: 'Pending Task',
70 | status: 'pending' as const,
71 | dependencies: []
72 | }
73 | ];
74 |
75 | // Act
76 | const stats = calculateTaskStatistics(tasks);
77 |
78 | // Assert
79 | expect(stats.total).toBe(4);
80 | expect(stats.done).toBe(1);
81 | expect(stats.cancelled).toBe(1);
82 | expect(stats.completedCount).toBe(3); // done + completed + cancelled
83 | // 3 complete out of 4 total = 75%
84 | expect(stats.completionPercentage).toBe(75);
85 | });
86 |
87 | it('should show 100% completion when all tasks are cancelled', () => {
88 | // Arrange
89 | const tasks: Task[] = [
90 | {
91 | id: 1,
92 | title: 'Cancelled 1',
93 | status: 'cancelled' as const,
94 | dependencies: []
95 | },
96 | {
97 | id: 2,
98 | title: 'Cancelled 2',
99 | status: 'cancelled' as const,
100 | dependencies: []
101 | }
102 | ];
103 |
104 | // Act
105 | const stats = calculateTaskStatistics(tasks);
106 |
107 | // Assert
108 | expect(stats.total).toBe(2);
109 | expect(stats.cancelled).toBe(2);
110 | expect(stats.completedCount).toBe(2); // All cancelled = all complete
111 | // BUG: Current code shows 0%, should be 100%
112 | expect(stats.completionPercentage).toBe(100);
113 | });
114 |
115 | it('should show 0% completion when no tasks are complete', () => {
116 | // Arrange
117 | const tasks: Task[] = [
118 | {
119 | id: 1,
120 | title: 'Pending Task',
121 | status: 'pending' as const,
122 | dependencies: []
123 | },
124 | {
125 | id: 2,
126 | title: 'In Progress Task',
127 | status: 'in-progress' as const,
128 | dependencies: []
129 | }
130 | ];
131 |
132 | // Act
133 | const stats = calculateTaskStatistics(tasks);
134 |
135 | // Assert
136 | expect(stats.completionPercentage).toBe(0);
137 | });
138 | });
139 |
140 | describe('calculateSubtaskStatistics', () => {
141 | it('should treat cancelled subtasks as complete in percentage calculation', () => {
142 | // Arrange: Task with 3 done subtasks and 1 cancelled = 100%
143 | const tasks: Task[] = [
144 | {
145 | id: 1,
146 | title: 'Parent Task',
147 | status: 'in-progress' as const,
148 | dependencies: [],
149 | subtasks: [
150 | { id: '1', title: 'Sub 1', status: 'done' },
151 | { id: '2', title: 'Sub 2', status: 'done' },
152 | { id: '3', title: 'Sub 3', status: 'done' },
153 | { id: '4', title: 'Sub 4', status: 'cancelled' }
154 | ]
155 | }
156 | ];
157 |
158 | // Act
159 | const stats = calculateSubtaskStatistics(tasks);
160 |
161 | // Assert
162 | expect(stats.total).toBe(4);
163 | expect(stats.done).toBe(3);
164 | expect(stats.cancelled).toBe(1);
165 | expect(stats.completedCount).toBe(4); // done + cancelled
166 | // BUG: Current code shows 75% (3/4), should be 100% (4/4)
167 | expect(stats.completionPercentage).toBe(100);
168 | });
169 |
170 | it('should handle completed status in subtasks', () => {
171 | // Arrange
172 | const tasks: Task[] = [
173 | {
174 | id: 1,
175 | title: 'Parent Task',
176 | status: 'in-progress' as const,
177 | dependencies: [],
178 | subtasks: [
179 | { id: '1', title: 'Sub 1', status: 'done' },
180 | { id: '2', title: 'Sub 2', status: 'completed' },
181 | { id: '3', title: 'Sub 3', status: 'pending' }
182 | ]
183 | }
184 | ];
185 |
186 | // Act
187 | const stats = calculateSubtaskStatistics(tasks);
188 |
189 | // Assert
190 | expect(stats.total).toBe(3);
191 | expect(stats.completedCount).toBe(2); // done + completed
192 | // 2 complete (done + completed) out of 3 = 67%
193 | expect(stats.completionPercentage).toBe(67);
194 | });
195 | });
196 |
197 | describe('calculateDependencyStatistics', () => {
198 | it('should treat cancelled tasks as satisfied dependencies', () => {
199 | // Arrange: Task 15 depends on cancelled task 14
200 | const tasks: Task[] = [
201 | ...Array.from({ length: 13 }, (_, i) => ({
202 | id: i + 1,
203 | title: `Task ${i + 1}`,
204 | status: 'done' as const,
205 | dependencies: []
206 | })),
207 | {
208 | id: 14,
209 | title: 'Cancelled Dependency',
210 | status: 'cancelled' as const,
211 | dependencies: []
212 | },
213 | {
214 | id: 15,
215 | title: 'Dependent Task',
216 | status: 'pending' as const,
217 | dependencies: [14]
218 | }
219 | ];
220 |
221 | // Act
222 | const stats = calculateDependencyStatistics(tasks);
223 |
224 | // Assert
225 | // Task 15 should be ready to work on since its dependency (14) is cancelled
226 | // BUG: Current code shows task 15 as blocked, should show as ready
227 | expect(stats.tasksBlockedByDeps).toBe(0);
228 | expect(stats.tasksReadyToWork).toBeGreaterThan(0);
229 | });
230 |
231 | it('should treat completed status as satisfied dependencies', () => {
232 | // Arrange
233 | const tasks: Task[] = [
234 | {
235 | id: 1,
236 | title: 'Completed Dependency',
237 | status: 'completed' as const,
238 | dependencies: []
239 | },
240 | {
241 | id: 2,
242 | title: 'Dependent Task',
243 | status: 'pending' as const,
244 | dependencies: [1]
245 | }
246 | ];
247 |
248 | // Act
249 | const stats = calculateDependencyStatistics(tasks);
250 |
251 | // Assert
252 | expect(stats.tasksBlockedByDeps).toBe(0);
253 | expect(stats.tasksReadyToWork).toBe(1);
254 | });
255 |
256 | it('should count tasks with cancelled dependencies as ready', () => {
257 | // Arrange: Multiple tasks depending on cancelled tasks
258 | const tasks: Task[] = [
259 | {
260 | id: 1,
261 | title: 'Cancelled Task',
262 | status: 'cancelled' as const,
263 | dependencies: []
264 | },
265 | {
266 | id: 2,
267 | title: 'Dependent 1',
268 | status: 'pending' as const,
269 | dependencies: [1]
270 | },
271 | {
272 | id: 3,
273 | title: 'Dependent 2',
274 | status: 'pending' as const,
275 | dependencies: [1]
276 | }
277 | ];
278 |
279 | // Act
280 | const stats = calculateDependencyStatistics(tasks);
281 |
282 | // Assert
283 | expect(stats.tasksBlockedByDeps).toBe(0);
284 | expect(stats.tasksReadyToWork).toBe(2); // Both dependents should be ready
285 | });
286 | });
287 | });
288 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/managers/config-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Configuration Manager
3 | * Orchestrates configuration services following clean architecture principles
4 | *
5 | * This ConfigManager delegates responsibilities to specialized services for better
6 | * maintainability, testability, and separation of concerns.
7 | */
8 |
9 | import type {
10 | PartialConfiguration,
11 | RuntimeStorageConfig
12 | } from '../../../common/interfaces/configuration.interface.js';
13 | import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../../../common/interfaces/configuration.interface.js';
14 | import { ConfigLoader } from '../services/config-loader.service.js';
15 | import {
16 | CONFIG_PRECEDENCE,
17 | ConfigMerger
18 | } from '../services/config-merger.service.js';
19 | import { ConfigPersistence } from '../services/config-persistence.service.js';
20 | import { EnvironmentConfigProvider } from '../services/environment-config-provider.service.js';
21 | import { RuntimeStateManager } from '../services/runtime-state-manager.service.js';
22 |
23 | /**
24 | * ConfigManager orchestrates all configuration services
25 | *
26 | * This class delegates responsibilities to specialized services:
27 | * - ConfigLoader: Loads configuration from files
28 | * - ConfigMerger: Merges configurations with precedence
29 | * - RuntimeStateManager: Manages runtime state
30 | * - ConfigPersistence: Handles file persistence
31 | * - EnvironmentConfigProvider: Extracts env var configuration
32 | */
33 | export class ConfigManager {
34 | private projectRoot: string;
35 | private config: PartialConfiguration = {};
36 | private initialized = false;
37 |
38 | // Services
39 | private loader: ConfigLoader;
40 | private merger: ConfigMerger;
41 | private stateManager: RuntimeStateManager;
42 | private persistence: ConfigPersistence;
43 | private envProvider: EnvironmentConfigProvider;
44 |
45 | /**
46 | * Create and initialize a new ConfigManager instance
47 | * This is the ONLY way to create a ConfigManager
48 | *
49 | * @param projectRoot - The root directory of the project
50 | * @returns Fully initialized ConfigManager instance
51 | */
52 | static async create(projectRoot: string): Promise<ConfigManager> {
53 | const manager = new ConfigManager(projectRoot);
54 | await manager.initialize();
55 | return manager;
56 | }
57 |
58 | /**
59 | * Private constructor - use ConfigManager.create() instead
60 | * This ensures the ConfigManager is always properly initialized
61 | */
62 | private constructor(projectRoot: string) {
63 | this.projectRoot = projectRoot;
64 |
65 | // Initialize services
66 | this.loader = new ConfigLoader(projectRoot);
67 | this.merger = new ConfigMerger();
68 | this.stateManager = new RuntimeStateManager(projectRoot);
69 | this.persistence = new ConfigPersistence(projectRoot);
70 | this.envProvider = new EnvironmentConfigProvider();
71 | }
72 |
73 | /**
74 | * Initialize by loading configuration from all sources
75 | * Private - only called by the factory method
76 | */
77 | private async initialize(): Promise<void> {
78 | if (this.initialized) return;
79 |
80 | // Clear any existing configuration sources
81 | this.merger.clearSources();
82 |
83 | // 1. Load default configuration (lowest precedence)
84 | this.merger.addSource({
85 | name: 'defaults',
86 | config: this.loader.getDefaultConfig(),
87 | precedence: CONFIG_PRECEDENCE.DEFAULTS
88 | });
89 |
90 | // 2. Load global configuration (if exists)
91 | const globalConfig = await this.loader.loadGlobalConfig();
92 | if (globalConfig) {
93 | this.merger.addSource({
94 | name: 'global',
95 | config: globalConfig,
96 | precedence: CONFIG_PRECEDENCE.GLOBAL
97 | });
98 | }
99 |
100 | // 3. Load local project configuration
101 | const localConfig = await this.loader.loadLocalConfig();
102 | if (localConfig) {
103 | this.merger.addSource({
104 | name: 'local',
105 | config: localConfig,
106 | precedence: CONFIG_PRECEDENCE.LOCAL
107 | });
108 | }
109 |
110 | // 4. Load environment variables (highest precedence)
111 | const envConfig = this.envProvider.loadConfig();
112 | if (Object.keys(envConfig).length > 0) {
113 | this.merger.addSource({
114 | name: 'environment',
115 | config: envConfig,
116 | precedence: CONFIG_PRECEDENCE.ENVIRONMENT
117 | });
118 | }
119 |
120 | // 5. Merge all configurations
121 | this.config = this.merger.merge();
122 |
123 | // 6. Load runtime state
124 | await this.stateManager.loadState();
125 |
126 | this.initialized = true;
127 | }
128 |
129 | // ==================== Configuration Access ====================
130 |
131 | /**
132 | * Get full configuration
133 | */
134 | getConfig(): PartialConfiguration {
135 | return this.config;
136 | }
137 |
138 | /**
139 | * Get storage configuration
140 | */
141 | getStorageConfig(): RuntimeStorageConfig {
142 | const storage = this.config.storage;
143 |
144 | // Return the configured type (including 'auto')
145 | const storageType = storage?.type || 'auto';
146 | const basePath = storage?.basePath ?? this.projectRoot;
147 |
148 | if (storageType === 'api' || storageType === 'auto') {
149 | return {
150 | type: storageType,
151 | basePath,
152 | apiEndpoint: storage?.apiEndpoint,
153 | apiAccessToken: storage?.apiAccessToken,
154 | apiConfigured: Boolean(storage?.apiEndpoint || storage?.apiAccessToken)
155 | };
156 | }
157 |
158 | return {
159 | type: storageType,
160 | basePath,
161 | apiConfigured: false
162 | };
163 | }
164 |
165 | /**
166 | * Get model configuration
167 | */
168 | getModelConfig() {
169 | return (
170 | this.config.models || {
171 | main: DEFAULTS.MODELS.MAIN,
172 | fallback: DEFAULTS.MODELS.FALLBACK
173 | }
174 | );
175 | }
176 |
177 | /**
178 | * Get response language setting
179 | */
180 | getResponseLanguage(): string {
181 | const customConfig = this.config.custom as any;
182 | return customConfig?.responseLanguage || 'English';
183 | }
184 |
185 | /**
186 | * Get project root path
187 | */
188 | getProjectRoot(): string {
189 | return this.projectRoot;
190 | }
191 |
192 | /**
193 | * Check if explicitly configured to use API storage
194 | * Excludes 'auto' type
195 | */
196 | isApiExplicitlyConfigured(): boolean {
197 | return this.getStorageConfig().type === 'api';
198 | }
199 |
200 | // ==================== Runtime State ====================
201 |
202 | /**
203 | * Get the currently active tag
204 | */
205 | getActiveTag(): string {
206 | return this.stateManager.getCurrentTag();
207 | }
208 |
209 | /**
210 | * Set the active tag
211 | */
212 | async setActiveTag(tag: string): Promise<void> {
213 | await this.stateManager.setCurrentTag(tag);
214 | }
215 |
216 | // ==================== Configuration Updates ====================
217 |
218 | /**
219 | * Update configuration
220 | */
221 | async updateConfig(updates: PartialConfiguration): Promise<void> {
222 | // Merge updates into current config
223 | Object.assign(this.config, updates);
224 |
225 | // Save to persistence
226 | await this.persistence.saveConfig(this.config);
227 |
228 | // Re-initialize to respect precedence
229 | this.initialized = false;
230 | await this.initialize();
231 | }
232 |
233 | /**
234 | * Set response language
235 | */
236 | async setResponseLanguage(language: string): Promise<void> {
237 | if (!this.config.custom) {
238 | this.config.custom = {};
239 | }
240 | (this.config.custom as any).responseLanguage = language;
241 | await this.persistence.saveConfig(this.config);
242 | }
243 |
244 | /**
245 | * Save current configuration
246 | */
247 | async saveConfig(): Promise<void> {
248 | await this.persistence.saveConfig(this.config, {
249 | createBackup: true,
250 | atomic: true
251 | });
252 | }
253 |
254 | // ==================== Utilities ====================
255 |
256 | /**
257 | * Reset configuration to defaults
258 | */
259 | async reset(): Promise<void> {
260 | // Clear configuration file
261 | await this.persistence.deleteConfig();
262 |
263 | // Clear runtime state
264 | await this.stateManager.clearState();
265 |
266 | // Reset internal state
267 | this.initialized = false;
268 | this.config = {};
269 |
270 | // Re-initialize with defaults
271 | await this.initialize();
272 | }
273 |
274 | /**
275 | * Get configuration sources for debugging
276 | */
277 | getConfigSources() {
278 | return this.merger.getSources();
279 | }
280 | }
281 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/auth/services/supabase-session-storage.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tests for SupabaseSessionStorage
3 | * Verifies session persistence with steno atomic writes
4 | */
5 |
6 | import { afterEach, beforeEach, describe, expect, it } from 'vitest';
7 | import fs from 'fs/promises';
8 | import fsSync from 'fs';
9 | import os from 'os';
10 | import path from 'path';
11 | import { SupabaseSessionStorage } from './supabase-session-storage.js';
12 |
13 | describe('SupabaseSessionStorage', () => {
14 | let tempDir: string;
15 | let sessionPath: string;
16 |
17 | beforeEach(() => {
18 | // Create unique temp directory for each test
19 | tempDir = fsSync.mkdtempSync(path.join(os.tmpdir(), 'tm-session-test-'));
20 | sessionPath = path.join(tempDir, 'session.json');
21 | });
22 |
23 | afterEach(() => {
24 | // Clean up temp directory
25 | if (fsSync.existsSync(tempDir)) {
26 | fsSync.rmSync(tempDir, { recursive: true, force: true });
27 | }
28 | });
29 |
30 | describe('persistence with steno', () => {
31 | it('should immediately persist data to disk with setItem', async () => {
32 | const storage = new SupabaseSessionStorage(sessionPath);
33 |
34 | // Set a session token
35 | const testSession = JSON.stringify({
36 | access_token: 'test-token',
37 | refresh_token: 'test-refresh'
38 | });
39 | await storage.setItem('sb-localhost-auth-token', testSession);
40 |
41 | // Immediately verify file exists and is readable (without any delay)
42 | expect(fsSync.existsSync(sessionPath)).toBe(true);
43 |
44 | // Read directly from disk (simulating a new process)
45 | const diskData = JSON.parse(fsSync.readFileSync(sessionPath, 'utf8'));
46 | expect(diskData['sb-localhost-auth-token']).toBe(testSession);
47 | });
48 |
49 | it('should guarantee data is on disk before returning', async () => {
50 | const storage = new SupabaseSessionStorage(sessionPath);
51 |
52 | // Write large session data
53 | const largeSession = JSON.stringify({
54 | access_token: 'x'.repeat(10000),
55 | refresh_token: 'y'.repeat(10000),
56 | user: { id: 'test', email: '[email protected]' }
57 | });
58 |
59 | await storage.setItem('sb-localhost-auth-token', largeSession);
60 |
61 | // Create a NEW storage instance (simulates separate CLI command)
62 | const newStorage = new SupabaseSessionStorage(sessionPath);
63 |
64 | // Should immediately read the persisted data
65 | const retrieved = await newStorage.getItem('sb-localhost-auth-token');
66 | expect(retrieved).toBe(largeSession);
67 | });
68 |
69 | it('should handle rapid sequential writes without data loss', async () => {
70 | const storage = new SupabaseSessionStorage(sessionPath);
71 |
72 | // Simulate rapid token updates (like during refresh)
73 | for (let i = 0; i < 5; i++) {
74 | const session = JSON.stringify({
75 | access_token: `token-${i}`,
76 | expires_at: Date.now() + i * 1000
77 | });
78 | await storage.setItem('sb-localhost-auth-token', session);
79 |
80 | // Each write should be immediately readable
81 | const newStorage = new SupabaseSessionStorage(sessionPath);
82 | const retrieved = await newStorage.getItem('sb-localhost-auth-token');
83 | expect(JSON.parse(retrieved!).access_token).toBe(`token-${i}`);
84 | }
85 | });
86 | });
87 |
88 | describe('atomic writes', () => {
89 | it('should complete writes atomically without leaving temp files', async () => {
90 | const storage = new SupabaseSessionStorage(sessionPath);
91 |
92 | await storage.setItem('test-key', 'test-value');
93 |
94 | // Final file should exist with correct content
95 | expect(fsSync.existsSync(sessionPath)).toBe(true);
96 | const diskData = JSON.parse(fsSync.readFileSync(sessionPath, 'utf8'));
97 | expect(diskData['test-key']).toBe('test-value');
98 |
99 | // No unexpected extra files should remain in directory
100 | const files = fsSync.readdirSync(path.dirname(sessionPath));
101 | expect(files).toEqual([path.basename(sessionPath)]);
102 | });
103 |
104 | it('should maintain correct file permissions (0700 for directory)', async () => {
105 | const storage = new SupabaseSessionStorage(sessionPath);
106 |
107 | await storage.setItem('test-key', 'test-value');
108 |
109 | // Check that file exists
110 | expect(fsSync.existsSync(sessionPath)).toBe(true);
111 |
112 | // Check directory has correct permissions
113 | const dir = path.dirname(sessionPath);
114 | const stats = fsSync.statSync(dir);
115 | const mode = stats.mode & 0o777;
116 |
117 | // Directory should be readable/writable/executable by owner only (0700)
118 | expect(mode).toBe(0o700);
119 | });
120 | });
121 |
122 | describe('basic operations', () => {
123 | it('should get and set items', async () => {
124 | const storage = new SupabaseSessionStorage(sessionPath);
125 |
126 | await storage.setItem('key1', 'value1');
127 | expect(await storage.getItem('key1')).toBe('value1');
128 | });
129 |
130 | it('should return null for non-existent items', async () => {
131 | const storage = new SupabaseSessionStorage(sessionPath);
132 |
133 | expect(await storage.getItem('non-existent')).toBe(null);
134 | });
135 |
136 | it('should remove items', async () => {
137 | const storage = new SupabaseSessionStorage(sessionPath);
138 |
139 | await storage.setItem('key1', 'value1');
140 | expect(await storage.getItem('key1')).toBe('value1');
141 |
142 | await storage.removeItem('key1');
143 | expect(await storage.getItem('key1')).toBe(null);
144 |
145 | // Verify removed from disk
146 | const newStorage = new SupabaseSessionStorage(sessionPath);
147 | expect(await newStorage.getItem('key1')).toBe(null);
148 | });
149 | });
150 |
151 | describe('initialization', () => {
152 | it('should load existing session on initialization', async () => {
153 | // Create initial storage and save data
154 | const storage1 = new SupabaseSessionStorage(sessionPath);
155 | await storage1.setItem('key1', 'value1');
156 |
157 | // Create new instance (simulates new process)
158 | const storage2 = new SupabaseSessionStorage(sessionPath);
159 | expect(await storage2.getItem('key1')).toBe('value1');
160 | });
161 |
162 | it('should handle non-existent session file gracefully', async () => {
163 | // Don't create any file, just initialize
164 | const storage = new SupabaseSessionStorage(sessionPath);
165 |
166 | // Should not throw and should work normally
167 | expect(await storage.getItem('any-key')).toBe(null);
168 | await storage.setItem('key1', 'value1');
169 | expect(await storage.getItem('key1')).toBe('value1');
170 | });
171 |
172 | it('should create directory if it does not exist', async () => {
173 | const deepPath = path.join(
174 | tempDir,
175 | 'deep',
176 | 'nested',
177 | 'path',
178 | 'session.json'
179 | );
180 | const storage = new SupabaseSessionStorage(deepPath);
181 |
182 | await storage.setItem('key1', 'value1');
183 |
184 | expect(fsSync.existsSync(deepPath)).toBe(true);
185 | expect(fsSync.existsSync(path.dirname(deepPath))).toBe(true);
186 | });
187 | });
188 |
189 | describe('error handling', () => {
190 | it('should not throw on persist errors', async () => {
191 | const invalidPath = '/invalid/path/that/cannot/be/written/session.json';
192 | const storage = new SupabaseSessionStorage(invalidPath);
193 |
194 | // Should not throw, session should remain in memory
195 | await storage.setItem('key1', 'value1');
196 |
197 | // Should still work in memory
198 | expect(await storage.getItem('key1')).toBe('value1');
199 | });
200 |
201 | it('should handle corrupted session file gracefully', async () => {
202 | // Write corrupted JSON
203 | fsSync.writeFileSync(sessionPath, 'invalid json {{{');
204 |
205 | // Should not throw on initialization
206 | expect(() => new SupabaseSessionStorage(sessionPath)).not.toThrow();
207 |
208 | // Should work normally after initialization
209 | const storage = new SupabaseSessionStorage(sessionPath);
210 | await storage.setItem('key1', 'value1');
211 | expect(await storage.getItem('key1')).toBe('value1');
212 | });
213 | });
214 | });
215 |
```
--------------------------------------------------------------------------------
/apps/mcp/src/shared/utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Shared utilities for MCP tools
3 | */
4 |
5 | import type { ContentResult } from 'fastmcp';
6 | import path from 'node:path';
7 | import fs from 'node:fs';
8 | import packageJson from '../../../../package.json' with { type: 'json' };
9 |
10 | /**
11 | * Get version information
12 | */
13 | export function getVersionInfo() {
14 | return {
15 | version: packageJson.version || 'unknown',
16 | name: packageJson.name || 'task-master-ai'
17 | };
18 | }
19 |
20 | /**
21 | * Get current tag for a project root
22 | */
23 | export function getCurrentTag(projectRoot: string): string | null {
24 | try {
25 | // Try to read current tag from state.json
26 | const stateJsonPath = path.join(projectRoot, '.taskmaster', 'state.json');
27 |
28 | if (fs.existsSync(stateJsonPath)) {
29 | const stateData = JSON.parse(fs.readFileSync(stateJsonPath, 'utf-8'));
30 | return stateData.currentTag || 'master';
31 | }
32 |
33 | return 'master';
34 | } catch {
35 | return null;
36 | }
37 | }
38 |
39 | /**
40 | * Handle API result with standardized error handling and response formatting
41 | * This provides a consistent response structure for all MCP tools
42 | */
43 | export async function handleApiResult<T>(options: {
44 | result: { success: boolean; data?: T; error?: { message: string } };
45 | log?: any;
46 | errorPrefix?: string;
47 | projectRoot?: string;
48 | tag?: string; // Optional tag/brief to use instead of reading from state.json
49 | }): Promise<ContentResult> {
50 | const {
51 | result,
52 | log,
53 | errorPrefix = 'API error',
54 | projectRoot,
55 | tag: providedTag
56 | } = options;
57 |
58 | // Get version info for every response
59 | const versionInfo = getVersionInfo();
60 |
61 | // Use provided tag if available, otherwise get from state.json
62 | // Note: For API storage, tm-core returns the brief name as the tag
63 | const currentTag =
64 | providedTag !== undefined
65 | ? providedTag
66 | : projectRoot
67 | ? getCurrentTag(projectRoot)
68 | : null;
69 |
70 | if (!result.success) {
71 | const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
72 | log?.error?.(`${errorPrefix}: ${errorMsg}`);
73 |
74 | let errorText = `Error: ${errorMsg}\nVersion: ${versionInfo.version}\nName: ${versionInfo.name}`;
75 |
76 | if (currentTag) {
77 | errorText += `\nCurrent Tag: ${currentTag}`;
78 | }
79 |
80 | return {
81 | content: [
82 | {
83 | type: 'text',
84 | text: errorText
85 | }
86 | ],
87 | isError: true
88 | };
89 | }
90 |
91 | log?.info?.('Successfully completed operation');
92 |
93 | // Create the response payload including version info and tag
94 | const responsePayload: any = {
95 | data: result.data,
96 | version: versionInfo
97 | };
98 |
99 | // Add current tag if available
100 | if (currentTag) {
101 | responsePayload.tag = currentTag;
102 | }
103 |
104 | return {
105 | content: [
106 | {
107 | type: 'text',
108 | text: JSON.stringify(responsePayload, null, 2)
109 | }
110 | ]
111 | };
112 | }
113 |
114 | /**
115 | * Normalize project root path (handles URI encoding, file:// protocol, Windows paths)
116 | */
117 | export function normalizeProjectRoot(rawPath: string): string {
118 | if (!rawPath) return process.cwd();
119 |
120 | try {
121 | let pathString = rawPath;
122 |
123 | // Decode URI encoding
124 | try {
125 | pathString = decodeURIComponent(pathString);
126 | } catch {
127 | // If decoding fails, use as-is
128 | }
129 |
130 | // Strip file:// prefix
131 | if (pathString.startsWith('file:///')) {
132 | pathString = pathString.slice(7);
133 | } else if (pathString.startsWith('file://')) {
134 | pathString = pathString.slice(7);
135 | }
136 |
137 | // Handle Windows drive letter after stripping prefix (e.g., /C:/...)
138 | if (
139 | pathString.startsWith('/') &&
140 | /[A-Za-z]:/.test(pathString.substring(1, 3))
141 | ) {
142 | pathString = pathString.substring(1);
143 | }
144 |
145 | // Normalize backslashes to forward slashes
146 | pathString = pathString.replace(/\\/g, '/');
147 |
148 | // Resolve to absolute path
149 | return path.resolve(pathString);
150 | } catch {
151 | return path.resolve(rawPath);
152 | }
153 | }
154 |
155 | /**
156 | * Get project root from session object
157 | */
158 | function getProjectRootFromSession(session: any): string | null {
159 | try {
160 | // Check primary location
161 | if (session?.roots?.[0]?.uri) {
162 | return normalizeProjectRoot(session.roots[0].uri);
163 | }
164 | // Check alternate location
165 | else if (session?.roots?.roots?.[0]?.uri) {
166 | return normalizeProjectRoot(session.roots.roots[0].uri);
167 | }
168 | return null;
169 | } catch {
170 | return null;
171 | }
172 | }
173 |
174 | /**
175 | * Wrapper to normalize project root in args with proper precedence order
176 | *
177 | * PRECEDENCE ORDER:
178 | * 1. TASK_MASTER_PROJECT_ROOT environment variable (from process.env or session)
179 | * 2. args.projectRoot (explicitly provided)
180 | * 3. Session-based project root resolution
181 | * 4. Current directory fallback
182 | */
183 | export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
184 | fn: (
185 | args: T & { projectRoot: string },
186 | context: any
187 | ) => Promise<ContentResult>
188 | ): (args: T, context: any) => Promise<ContentResult> {
189 | return async (args: T, context: any): Promise<ContentResult> => {
190 | const { log, session } = context;
191 | let normalizedRoot: string | null = null;
192 | let rootSource = 'unknown';
193 |
194 | try {
195 | // 1. Check for TASK_MASTER_PROJECT_ROOT environment variable first
196 | if (process.env.TASK_MASTER_PROJECT_ROOT) {
197 | const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
198 | normalizedRoot = path.isAbsolute(envRoot)
199 | ? envRoot
200 | : path.resolve(process.cwd(), envRoot);
201 | rootSource = 'TASK_MASTER_PROJECT_ROOT environment variable';
202 | log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
203 | }
204 | // Also check session environment variables for TASK_MASTER_PROJECT_ROOT
205 | else if (session?.env?.TASK_MASTER_PROJECT_ROOT) {
206 | const envRoot = session.env.TASK_MASTER_PROJECT_ROOT;
207 | normalizedRoot = path.isAbsolute(envRoot)
208 | ? envRoot
209 | : path.resolve(process.cwd(), envRoot);
210 | rootSource = 'TASK_MASTER_PROJECT_ROOT session environment variable';
211 | log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
212 | }
213 | // 2. If no environment variable, try args.projectRoot
214 | else if (args.projectRoot) {
215 | normalizedRoot = normalizeProjectRoot(args.projectRoot);
216 | rootSource = 'args.projectRoot';
217 | log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
218 | }
219 | // 3. If no args.projectRoot, try session-based resolution
220 | else {
221 | const sessionRoot = getProjectRootFromSession(session);
222 | if (sessionRoot) {
223 | normalizedRoot = sessionRoot;
224 | rootSource = 'session';
225 | log?.info?.(
226 | `Using project root from ${rootSource}: ${normalizedRoot}`
227 | );
228 | }
229 | }
230 |
231 | if (!normalizedRoot) {
232 | log?.error?.(
233 | 'Could not determine project root from environment, args, or session.'
234 | );
235 | return handleApiResult({
236 | result: {
237 | success: false,
238 | error: {
239 | message:
240 | 'Could not determine project root. Please provide projectRoot argument or ensure TASK_MASTER_PROJECT_ROOT environment variable is set.'
241 | }
242 | }
243 | });
244 | }
245 |
246 | // Inject the normalized root back into args
247 | const updatedArgs = { ...args, projectRoot: normalizedRoot } as T & {
248 | projectRoot: string;
249 | };
250 |
251 | // Execute the original function with normalized root in args
252 | return await fn(updatedArgs, context);
253 | } catch (error: any) {
254 | log?.error?.(
255 | `Error within withNormalizedProjectRoot HOF (Normalized Root: ${normalizedRoot}): ${error.message}`
256 | );
257 | if (error.stack && log?.debug) {
258 | log.debug(error.stack);
259 | }
260 | return handleApiResult({
261 | result: {
262 | success: false,
263 | error: {
264 | message: `Operation failed: ${error.message}`
265 | }
266 | }
267 | });
268 | }
269 | };
270 | }
271 |
```