This is page 19 of 50. Use http://codebase.md/eyaltoledano/claude-task-master?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/integration/clients/supabase-client.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Supabase authentication client for CLI auth flows
*/
import {
Session,
SupabaseClient as SupabaseJSClient,
User,
createClient
} from '@supabase/supabase-js';
import { getLogger } from '../../../common/logger/index.js';
import { SupabaseSessionStorage } from '../../auth/services/supabase-session-storage.js';
import { AuthenticationError } from '../../auth/types.js';
export class SupabaseAuthClient {
private client: SupabaseJSClient | null = null;
private sessionStorage: SupabaseSessionStorage;
private logger = getLogger('SupabaseAuthClient');
constructor() {
this.sessionStorage = new SupabaseSessionStorage();
}
/**
* Get Supabase client with proper session management
*/
getClient(): SupabaseJSClient {
if (!this.client) {
// Get Supabase configuration from environment
// Runtime vars (TM_*) take precedence over build-time vars (TM_PUBLIC_*)
const supabaseUrl =
process.env.TM_SUPABASE_URL || process.env.TM_PUBLIC_SUPABASE_URL;
const supabaseAnonKey =
process.env.TM_SUPABASE_ANON_KEY ||
process.env.TM_PUBLIC_SUPABASE_ANON_KEY;
if (!supabaseUrl || !supabaseAnonKey) {
throw new AuthenticationError(
'Supabase configuration missing. Please set TM_SUPABASE_URL and TM_SUPABASE_ANON_KEY (runtime) or TM_PUBLIC_SUPABASE_URL and TM_PUBLIC_SUPABASE_ANON_KEY (build-time) environment variables.',
'CONFIG_MISSING'
);
}
// Create client with custom storage adapter (similar to React Native AsyncStorage)
this.client = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
storage: this.sessionStorage,
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: false
}
});
}
return this.client;
}
/**
* Initialize the client and restore session if available
*/
async initialize(): Promise<Session | null> {
const client = this.getClient();
try {
// Get the current session from storage
const {
data: { session },
error
} = await client.auth.getSession();
if (error) {
this.logger.warn('Failed to restore session:', error);
return null;
}
if (session) {
this.logger.info('Session restored successfully');
}
return session;
} catch (error) {
this.logger.error('Error initializing session:', error);
return null;
}
}
/**
* Sign in with PKCE flow (for CLI auth)
*/
async signInWithPKCE(): Promise<{ url: string; codeVerifier: string }> {
const client = this.getClient();
try {
// Generate PKCE challenge
const { data, error } = await client.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo:
process.env.TM_AUTH_CALLBACK_URL ||
'http://localhost:3421/auth/callback',
scopes: 'email'
}
});
if (error) {
throw new AuthenticationError(
`Failed to initiate PKCE flow: ${error.message}`,
'PKCE_INIT_FAILED'
);
}
if (!data?.url) {
throw new AuthenticationError(
'No authorization URL returned',
'INVALID_RESPONSE'
);
}
// Extract code_verifier from the URL or generate it
// Note: Supabase handles PKCE internally, we just need to handle the callback
return {
url: data.url,
codeVerifier: '' // Supabase manages this internally
};
} catch (error) {
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
`Failed to start PKCE flow: ${(error as Error).message}`,
'PKCE_FAILED'
);
}
}
/**
* Exchange authorization code for session (PKCE flow)
*/
async exchangeCodeForSession(code: string): Promise<Session> {
const client = this.getClient();
try {
const { data, error } = await client.auth.exchangeCodeForSession(code);
if (error) {
throw new AuthenticationError(
`Failed to exchange code: ${error.message}`,
'CODE_EXCHANGE_FAILED'
);
}
if (!data?.session) {
throw new AuthenticationError(
'No session returned from code exchange',
'INVALID_RESPONSE'
);
}
this.logger.info('Successfully exchanged code for session');
return data.session;
} catch (error) {
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
`Code exchange failed: ${(error as Error).message}`,
'CODE_EXCHANGE_FAILED'
);
}
}
/**
* Get the current session
*/
async getSession(): Promise<Session | null> {
const client = this.getClient();
try {
const {
data: { session },
error
} = await client.auth.getSession();
if (error) {
this.logger.warn('Failed to get session:', error);
return null;
}
return session;
} catch (error) {
this.logger.error('Error getting session:', error);
return null;
}
}
/**
* Refresh the current session
*/
async refreshSession(): Promise<Session | null> {
const client = this.getClient();
try {
this.logger.info('Refreshing session...');
// Supabase will automatically use the stored refresh token
const {
data: { session },
error
} = await client.auth.refreshSession();
if (error) {
this.logger.error('Failed to refresh session:', error);
throw new AuthenticationError(
`Failed to refresh session: ${error.message}`,
'REFRESH_FAILED'
);
}
if (session) {
this.logger.info('Successfully refreshed session');
}
return session;
} catch (error) {
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
`Failed to refresh session: ${(error as Error).message}`,
'REFRESH_FAILED'
);
}
}
/**
* Get current user from session
*/
async getUser(): Promise<User | null> {
const client = this.getClient();
try {
const {
data: { user },
error
} = await client.auth.getUser();
if (error) {
this.logger.warn('Failed to get user:', error);
return null;
}
return user;
} catch (error) {
this.logger.error('Error getting user:', error);
return null;
}
}
/**
* Sign out and clear session
*/
async signOut(): Promise<void> {
const client = this.getClient();
try {
// Sign out with global scope to revoke all refresh tokens
const { error } = await client.auth.signOut({ scope: 'global' });
if (error) {
this.logger.warn('Failed to sign out:', error);
}
// Clear cached session data
this.sessionStorage.clear();
} catch (error) {
this.logger.error('Error during sign out:', error);
}
}
/**
* Set session from external auth (e.g., from server callback)
*/
async setSession(session: Session): Promise<void> {
const client = this.getClient();
try {
const { error } = await client.auth.setSession({
access_token: session.access_token,
refresh_token: session.refresh_token
});
if (error) {
throw new AuthenticationError(
`Failed to set session: ${error.message}`,
'SESSION_SET_FAILED'
);
}
this.logger.info('Session set successfully');
} catch (error) {
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
`Failed to set session: ${(error as Error).message}`,
'SESSION_SET_FAILED'
);
}
}
/**
* Verify a one-time token and create a session
* Used for CLI authentication with pre-generated tokens
*/
async verifyOneTimeCode(token: string): Promise<Session> {
const client = this.getClient();
try {
this.logger.info('Verifying authentication token...');
// Use Supabase's verifyOtp for token verification
// Using token_hash with magiclink type doesn't require email
const { data, error } = await client.auth.verifyOtp({
token_hash: token,
type: 'magiclink'
});
if (error) {
this.logger.error('Failed to verify token:', error);
throw new AuthenticationError(
`Failed to verify token: ${error.message}`,
'INVALID_CODE'
);
}
if (!data?.session) {
throw new AuthenticationError(
'No session returned from token verification',
'INVALID_RESPONSE'
);
}
this.logger.info('Successfully verified authentication token');
return data.session;
} catch (error) {
if (error instanceof AuthenticationError) {
throw error;
}
throw new AuthenticationError(
`Token verification failed: ${(error as Error).message}`,
'CODE_AUTH_FAILED'
);
}
}
}
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/utils/project-root-finder.spec.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Tests for project root finder utilities
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import path from 'node:path';
import fs from 'node:fs';
import os from 'node:os';
import {
findProjectRoot,
normalizeProjectRoot
} from './project-root-finder.js';
describe('findProjectRoot', () => {
let tempDir: string;
let originalCwd: string;
beforeEach(() => {
// Save original working directory
originalCwd = process.cwd();
// Create a temporary directory for testing
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-test-'));
});
afterEach(() => {
// Restore original working directory
process.chdir(originalCwd);
// Clean up temp directory
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('Task Master marker detection', () => {
it('should find .taskmaster directory in current directory', () => {
const taskmasterDir = path.join(tempDir, '.taskmaster');
fs.mkdirSync(taskmasterDir);
const result = findProjectRoot(tempDir);
expect(result).toBe(tempDir);
});
it('should find .taskmaster directory in parent directory', () => {
const parentDir = tempDir;
const childDir = path.join(tempDir, 'child');
const taskmasterDir = path.join(parentDir, '.taskmaster');
fs.mkdirSync(taskmasterDir);
fs.mkdirSync(childDir);
const result = findProjectRoot(childDir);
expect(result).toBe(parentDir);
});
it('should find .taskmaster/config.json marker', () => {
const configDir = path.join(tempDir, '.taskmaster');
fs.mkdirSync(configDir);
fs.writeFileSync(path.join(configDir, 'config.json'), '{}');
const result = findProjectRoot(tempDir);
expect(result).toBe(tempDir);
});
it('should find .taskmaster/tasks/tasks.json marker', () => {
const tasksDir = path.join(tempDir, '.taskmaster', 'tasks');
fs.mkdirSync(tasksDir, { recursive: true });
fs.writeFileSync(path.join(tasksDir, 'tasks.json'), '{}');
const result = findProjectRoot(tempDir);
expect(result).toBe(tempDir);
});
it('should find .taskmasterconfig (legacy) marker', () => {
fs.writeFileSync(path.join(tempDir, '.taskmasterconfig'), '{}');
const result = findProjectRoot(tempDir);
expect(result).toBe(tempDir);
});
});
describe('Monorepo behavior - Task Master markers take precedence', () => {
it('should find .taskmaster in parent when starting from apps subdirectory', () => {
// Simulate exact user scenario:
// /project/.taskmaster exists
// Starting from /project/apps
const projectRoot = tempDir;
const appsDir = path.join(tempDir, 'apps');
const taskmasterDir = path.join(projectRoot, '.taskmaster');
fs.mkdirSync(taskmasterDir);
fs.mkdirSync(appsDir);
// When called from apps directory
const result = findProjectRoot(appsDir);
// Should return project root (one level up)
expect(result).toBe(projectRoot);
});
it('should prioritize .taskmaster in parent over .git in child', () => {
// Create structure: /parent/.taskmaster and /parent/child/.git
const parentDir = tempDir;
const childDir = path.join(tempDir, 'child');
const gitDir = path.join(childDir, '.git');
const taskmasterDir = path.join(parentDir, '.taskmaster');
fs.mkdirSync(taskmasterDir);
fs.mkdirSync(childDir);
fs.mkdirSync(gitDir);
// When called from child directory
const result = findProjectRoot(childDir);
// Should return parent (with .taskmaster), not child (with .git)
expect(result).toBe(parentDir);
});
it('should prioritize .taskmaster in grandparent over package.json in child', () => {
// Create structure: /grandparent/.taskmaster and /grandparent/parent/child/package.json
const grandparentDir = tempDir;
const parentDir = path.join(tempDir, 'parent');
const childDir = path.join(parentDir, 'child');
const taskmasterDir = path.join(grandparentDir, '.taskmaster');
fs.mkdirSync(taskmasterDir);
fs.mkdirSync(parentDir);
fs.mkdirSync(childDir);
fs.writeFileSync(path.join(childDir, 'package.json'), '{}');
const result = findProjectRoot(childDir);
expect(result).toBe(grandparentDir);
});
it('should prioritize .taskmaster over multiple other project markers', () => {
// Create structure with many markers
const parentDir = tempDir;
const childDir = path.join(tempDir, 'packages', 'my-package');
const taskmasterDir = path.join(parentDir, '.taskmaster');
fs.mkdirSync(taskmasterDir);
fs.mkdirSync(childDir, { recursive: true });
// Add multiple other project markers in child
fs.mkdirSync(path.join(childDir, '.git'));
fs.writeFileSync(path.join(childDir, 'package.json'), '{}');
fs.writeFileSync(path.join(childDir, 'go.mod'), '');
fs.writeFileSync(path.join(childDir, 'Cargo.toml'), '');
const result = findProjectRoot(childDir);
// Should still return parent with .taskmaster
expect(result).toBe(parentDir);
});
});
describe('Other project marker detection (when no Task Master markers)', () => {
it('should find .git directory', () => {
const gitDir = path.join(tempDir, '.git');
fs.mkdirSync(gitDir);
const result = findProjectRoot(tempDir);
expect(result).toBe(tempDir);
});
it('should find package.json', () => {
fs.writeFileSync(path.join(tempDir, 'package.json'), '{}');
const result = findProjectRoot(tempDir);
expect(result).toBe(tempDir);
});
it('should find go.mod', () => {
fs.writeFileSync(path.join(tempDir, 'go.mod'), '');
const result = findProjectRoot(tempDir);
expect(result).toBe(tempDir);
});
it('should find Cargo.toml (Rust)', () => {
fs.writeFileSync(path.join(tempDir, 'Cargo.toml'), '');
const result = findProjectRoot(tempDir);
expect(result).toBe(tempDir);
});
it('should find pyproject.toml (Python)', () => {
fs.writeFileSync(path.join(tempDir, 'pyproject.toml'), '');
const result = findProjectRoot(tempDir);
expect(result).toBe(tempDir);
});
});
describe('Edge cases', () => {
it('should return current directory if no markers found', () => {
const result = findProjectRoot(tempDir);
// Should fall back to process.cwd()
expect(result).toBe(process.cwd());
});
it('should handle permission errors gracefully', () => {
// This test is hard to implement portably, but the function should handle it
const result = findProjectRoot(tempDir);
expect(typeof result).toBe('string');
});
it('should not traverse more than 50 levels', () => {
// Create a deep directory structure
let deepDir = tempDir;
for (let i = 0; i < 60; i++) {
deepDir = path.join(deepDir, `level${i}`);
}
// Don't actually create it, just test the function doesn't hang
const result = findProjectRoot(deepDir);
expect(typeof result).toBe('string');
});
it('should handle being called from filesystem root', () => {
const rootDir = path.parse(tempDir).root;
const result = findProjectRoot(rootDir);
expect(typeof result).toBe('string');
});
});
});
describe('normalizeProjectRoot', () => {
it('should remove .taskmaster from path', () => {
const result = normalizeProjectRoot('/project/.taskmaster');
expect(result).toBe('/project');
});
it('should remove .taskmaster/subdirectory from path', () => {
const result = normalizeProjectRoot('/project/.taskmaster/tasks');
expect(result).toBe('/project');
});
it('should return unchanged path if no .taskmaster', () => {
const result = normalizeProjectRoot('/project/src');
expect(result).toBe('/project/src');
});
it('should handle paths with native separators', () => {
// Use native path separators for the test
const testPath = ['project', '.taskmaster', 'tasks'].join(path.sep);
const expectedPath = 'project';
const result = normalizeProjectRoot(testPath);
expect(result).toBe(expectedPath);
});
it('should handle empty string', () => {
const result = normalizeProjectRoot('');
expect(result).toBe('');
});
it('should handle null', () => {
const result = normalizeProjectRoot(null);
expect(result).toBe('');
});
it('should handle undefined', () => {
const result = normalizeProjectRoot(undefined);
expect(result).toBe('');
});
it('should handle root .taskmaster', () => {
const sep = path.sep;
const result = normalizeProjectRoot(`${sep}.taskmaster`);
expect(result).toBe(sep);
});
});
```
--------------------------------------------------------------------------------
/tests/e2e/parse_llm_output.cjs:
--------------------------------------------------------------------------------
```
#!/usr/bin/env node
// Note: We will use dynamic import() inside the async callback due to project being type: module
const readline = require('readline');
const path = require('path'); // Import path module
let inputData = '';
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
rl.on('line', (line) => {
inputData += line;
});
// Make the callback async to allow await for dynamic imports
rl.on('close', async () => {
let chalk, boxen, Table;
try {
// Dynamically import libraries
chalk = (await import('chalk')).default;
boxen = (await import('boxen')).default;
Table = (await import('cli-table3')).default;
// 1. Parse the initial API response body
const apiResponse = JSON.parse(inputData);
// 2. Extract the text content containing the nested JSON
// Robust check for content structure
const textContent = apiResponse?.content?.[0]?.text;
if (!textContent) {
console.error(
chalk.red(
"Error: Could not find '.content[0].text' in the API response JSON."
)
);
process.exit(1);
}
// 3. Find the start of the actual JSON block
const jsonStart = textContent.indexOf('{');
const jsonEnd = textContent.lastIndexOf('}');
if (jsonStart === -1 || jsonEnd === -1 || jsonEnd < jsonStart) {
console.error(
chalk.red(
'Error: Could not find JSON block starting with { and ending with } in the extracted text content.'
)
);
process.exit(1);
}
const jsonString = textContent.substring(jsonStart, jsonEnd + 1);
// 4. Parse the extracted JSON string
let reportData;
try {
reportData = JSON.parse(jsonString);
} catch (parseError) {
console.error(
chalk.red('Error: Failed to parse the extracted JSON block.')
);
console.error(chalk.red('Parse Error:'), parseError.message);
process.exit(1);
}
// Ensure reportData is an object
if (typeof reportData !== 'object' || reportData === null) {
console.error(
chalk.red('Error: Parsed report data is not a valid object.')
);
process.exit(1);
}
// --- Get Log File Path and Format Timestamp ---
const logFilePath = process.argv[2]; // Get the log file path argument
let formattedTime = 'Unknown';
if (logFilePath) {
const logBasename = path.basename(logFilePath);
const timestampMatch = logBasename.match(/e2e_run_(\d{8}_\d{6})\.log$/);
if (timestampMatch && timestampMatch[1]) {
const ts = timestampMatch[1]; // YYYYMMDD_HHMMSS
// Format into YYYY-MM-DD HH:MM:SS
formattedTime = `${ts.substring(0, 4)}-${ts.substring(4, 6)}-${ts.substring(6, 8)} ${ts.substring(9, 11)}:${ts.substring(11, 13)}:${ts.substring(13, 15)}`;
}
}
// --------------------------------------------
// 5. Generate CLI Report (with defensive checks)
console.log(
'\n' +
chalk.cyan.bold(
boxen(
`TASKMASTER E2E Log Analysis Report\nRun Time: ${chalk.yellow(formattedTime)}`, // Display formatted time
{
padding: 1,
borderStyle: 'double',
borderColor: 'cyan',
textAlign: 'center' // Center align title
}
)
) +
'\n'
);
// Overall Status
let statusColor = chalk.white;
const overallStatus = reportData.overall_status || 'Unknown'; // Default if missing
if (overallStatus === 'Success') statusColor = chalk.green.bold;
if (overallStatus === 'Warning') statusColor = chalk.yellow.bold;
if (overallStatus === 'Failure') statusColor = chalk.red.bold;
console.log(
boxen(`Overall Status: ${statusColor(overallStatus)}`, {
padding: { left: 1, right: 1 },
margin: { bottom: 1 },
borderColor: 'blue'
})
);
// LLM Summary Points
console.log(chalk.blue.bold('📋 Summary Points:'));
if (
Array.isArray(reportData.llm_summary_points) &&
reportData.llm_summary_points.length > 0
) {
reportData.llm_summary_points.forEach((point) => {
console.log(chalk.white(` - ${point || 'N/A'}`)); // Handle null/undefined points
});
} else {
console.log(chalk.gray(' No summary points provided.'));
}
console.log();
// Verified Steps
console.log(chalk.green.bold('✅ Verified Steps:'));
if (
Array.isArray(reportData.verified_steps) &&
reportData.verified_steps.length > 0
) {
reportData.verified_steps.forEach((step) => {
console.log(chalk.green(` - ${step || 'N/A'}`)); // Handle null/undefined steps
});
} else {
console.log(chalk.gray(' No verified steps listed.'));
}
console.log();
// Provider Add-Task Comparison
console.log(chalk.magenta.bold('🔄 Provider Add-Task Comparison:'));
const comp = reportData.provider_add_task_comparison;
if (typeof comp === 'object' && comp !== null) {
console.log(
chalk.white(` Prompt Used: ${comp.prompt_used || 'Not specified'}`)
);
console.log();
if (
typeof comp.provider_results === 'object' &&
comp.provider_results !== null &&
Object.keys(comp.provider_results).length > 0
) {
const providerTable = new Table({
head: ['Provider', 'Status', 'Task ID', 'Score', 'Notes'].map((h) =>
chalk.magenta.bold(h)
),
colWidths: [15, 18, 10, 12, 45],
style: { head: [], border: [] },
wordWrap: true
});
for (const provider in comp.provider_results) {
const result = comp.provider_results[provider] || {}; // Default to empty object if provider result is null/undefined
const status = result.status || 'Unknown';
const isSuccess = status === 'Success';
const statusIcon = isSuccess ? chalk.green('✅') : chalk.red('❌');
const statusText = isSuccess
? chalk.green(status)
: chalk.red(status);
providerTable.push([
chalk.white(provider),
`${statusIcon} ${statusText}`,
chalk.white(result.task_id || 'N/A'),
chalk.white(result.score || 'N/A'),
chalk.dim(result.notes || 'N/A')
]);
}
console.log(providerTable.toString());
console.log();
} else {
console.log(chalk.gray(' No provider results available.'));
console.log();
}
console.log(chalk.white.bold(` Comparison Summary:`));
console.log(chalk.white(` ${comp.comparison_summary || 'N/A'}`));
} else {
console.log(chalk.gray(' Provider comparison data not found.'));
}
console.log();
// Detected Issues
console.log(chalk.red.bold('🚨 Detected Issues:'));
if (
Array.isArray(reportData.detected_issues) &&
reportData.detected_issues.length > 0
) {
reportData.detected_issues.forEach((issue, index) => {
if (typeof issue !== 'object' || issue === null) return; // Skip invalid issue entries
const severity = issue.severity || 'Unknown';
let boxColor = 'blue';
let icon = 'ℹ️';
if (severity === 'Error') {
boxColor = 'red';
icon = '❌';
}
if (severity === 'Warning') {
boxColor = 'yellow';
icon = '⚠️';
}
let issueContent = `${chalk.bold('Description:')} ${chalk.white(issue.description || 'N/A')}`;
// Only add log context if it exists and is not empty
if (issue.log_context && String(issue.log_context).trim()) {
issueContent += `\n${chalk.bold('Log Context:')} \n${chalk.dim(String(issue.log_context).trim())}`;
}
console.log(
boxen(issueContent, {
title: `${icon} Issue ${index + 1}: [${severity}]`,
padding: 1,
margin: { top: 1, bottom: 0 },
borderColor: boxColor,
borderStyle: 'round'
})
);
});
console.log(); // Add final newline if issues exist
} else {
console.log(chalk.green(' No specific issues detected by the LLM.'));
}
console.log();
console.log(chalk.cyan.bold('========================================'));
console.log(chalk.cyan.bold(' End of LLM Report'));
console.log(chalk.cyan.bold('========================================\n'));
} catch (error) {
// Ensure chalk is available for error reporting, provide fallback
const errorChalk = chalk || { red: (t) => t, yellow: (t) => t };
console.error(
errorChalk.red('Error processing LLM response:'),
error.message
);
// Avoid printing potentially huge inputData here unless necessary for debugging
// console.error(errorChalk.yellow('Raw input data (first 500 chars):'), inputData.substring(0, 500));
process.exit(1);
}
});
// Handle potential errors during stdin reading
process.stdin.on('error', (err) => {
console.error('Error reading standard input:', err);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/apps/extension/src/services/error-handler.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Error Handler Service
* Centralized error handling with categorization and recovery strategies
*/
import * as vscode from 'vscode';
import type { ExtensionLogger } from '../utils/logger';
export enum ErrorSeverity {
LOW = 'low',
MEDIUM = 'medium',
HIGH = 'high',
CRITICAL = 'critical'
}
export enum ErrorCategory {
MCP_CONNECTION = 'mcp_connection',
CONFIGURATION = 'configuration',
TASK_LOADING = 'task_loading',
NETWORK = 'network',
INTERNAL = 'internal'
}
export interface ErrorContext {
category: ErrorCategory;
severity: ErrorSeverity;
message: string;
originalError?: Error | unknown;
operation?: string;
taskId?: string;
isRecoverable?: boolean;
suggestedActions?: string[];
}
export class ErrorHandler {
private errorLog: Map<string, ErrorContext> = new Map();
private errorId = 0;
constructor(private logger: ExtensionLogger) {}
/**
* Handle an error with appropriate logging and user notification
*/
handleError(context: ErrorContext): string {
const errorId = `error_${++this.errorId}`;
this.errorLog.set(errorId, context);
// Log to extension logger
this.logError(context);
// Show user notification if appropriate
this.notifyUser(context);
return errorId;
}
/**
* Log error based on severity
*/
private logError(context: ErrorContext): void {
const logMessage = `[${context.category}] ${context.message}`;
const details = {
operation: context.operation,
taskId: context.taskId,
error: context.originalError
};
switch (context.severity) {
case ErrorSeverity.CRITICAL:
case ErrorSeverity.HIGH:
this.logger.error(logMessage, details);
break;
case ErrorSeverity.MEDIUM:
this.logger.warn(logMessage, details);
break;
case ErrorSeverity.LOW:
this.logger.debug(logMessage, details);
break;
}
}
/**
* Show user notification based on severity and category
*/
/**
* Validate if an action is allowed
*/
private isValidAction(action: string): boolean {
// Define predefined valid actions
const predefinedActions = [
'Retry',
'Settings',
'Reload',
'Dismiss',
'View Logs',
'Report Issue'
];
// Check if it's a predefined action or a TaskMaster command
return predefinedActions.includes(action) || action.startsWith('tm.');
}
/**
* Filter and validate suggested actions
*/
private getValidActions(actions: string[]): string[] {
return actions.filter((action) => this.isValidAction(action));
}
private notifyUser(context: ErrorContext): void {
// Don't show low severity errors to users
if (context.severity === ErrorSeverity.LOW) {
return;
}
// Validate and filter suggested actions
const rawActions = context.suggestedActions || [];
const actions = this.getValidActions(rawActions);
// Log if any actions were filtered out
if (rawActions.length !== actions.length) {
this.logger.warn('Invalid actions filtered out:', {
original: rawActions,
filtered: actions,
removed: rawActions.filter((a) => !actions.includes(a))
});
}
switch (context.severity) {
case ErrorSeverity.CRITICAL:
vscode.window
.showErrorMessage(`TaskMaster: ${context.message}`, ...actions)
.then((action) => {
if (action) {
this.handleUserAction(action, context);
}
});
break;
case ErrorSeverity.HIGH:
if (context.category === ErrorCategory.MCP_CONNECTION) {
// Use validated actions or default actions for MCP connection
const mcpActions =
actions.length > 0 ? actions : ['Retry', 'Settings'];
vscode.window
.showWarningMessage(`TaskMaster: ${context.message}`, ...mcpActions)
.then((action) => {
if (action === 'Retry') {
vscode.commands.executeCommand('tm.reconnect');
} else if (action === 'Settings') {
vscode.commands.executeCommand('tm.openSettings');
} else if (action) {
this.handleUserAction(action, context);
}
});
} else {
// Show warning with validated actions
if (actions.length > 0) {
vscode.window
.showWarningMessage(`TaskMaster: ${context.message}`, ...actions)
.then((action) => {
if (action) {
this.handleUserAction(action, context);
}
});
} else {
vscode.window.showWarningMessage(`TaskMaster: ${context.message}`);
}
}
break;
case ErrorSeverity.MEDIUM:
// Only show medium errors for important categories
if (
[ErrorCategory.CONFIGURATION, ErrorCategory.TASK_LOADING].includes(
context.category
)
) {
if (actions.length > 0) {
vscode.window
.showInformationMessage(
`TaskMaster: ${context.message}`,
...actions
)
.then((action) => {
if (action) {
this.handleUserAction(action, context);
}
});
} else {
vscode.window.showInformationMessage(
`TaskMaster: ${context.message}`
);
}
}
break;
}
}
/**
* Handle user action from notification
*/
private handleUserAction(action: string, context: ErrorContext): void {
this.logger.debug(`User selected action: ${action}`, {
errorContext: context
});
// Handle predefined actions
switch (action) {
case 'Retry':
if (context.category === ErrorCategory.MCP_CONNECTION) {
vscode.commands.executeCommand('tm.reconnect');
} else {
vscode.commands.executeCommand('tm.refreshTasks');
}
break;
case 'Settings':
vscode.commands.executeCommand('tm.openSettings');
break;
case 'Reload':
vscode.commands.executeCommand('workbench.action.reloadWindow');
break;
case 'View Logs':
// Show error details in a modal dialog instead of output channel
this.showErrorDetails(context);
break;
case 'Report Issue':
const issueUrl = this.generateIssueUrl(context);
vscode.env.openExternal(vscode.Uri.parse(issueUrl));
break;
case 'Dismiss':
// No action needed
break;
default:
// Handle TaskMaster commands (tm.*)
if (action.startsWith('tm.')) {
void vscode.commands.executeCommand(action).then(
() => {},
(error: unknown) => {
this.logger.error(`Failed to execute command: ${action}`, error);
}
);
}
break;
}
}
/**
* Show detailed error information in a modal dialog
*/
private showErrorDetails(context: ErrorContext): void {
const details = [
`**Error Details**`,
``,
`Category: ${context.category}`,
`Severity: ${context.severity}`,
`Message: ${context.message}`,
context.operation ? `Operation: ${context.operation}` : '',
context.taskId ? `Task ID: ${context.taskId}` : '',
context.originalError ? `\nOriginal Error:\n${context.originalError}` : ''
]
.filter(Boolean)
.join('\n');
vscode.window.showInformationMessage(details, {
modal: true,
detail: details
});
}
/**
* Generate GitHub issue URL with pre-filled information
*/
private generateIssueUrl(context: ErrorContext): string {
const title = encodeURIComponent(`[Extension Error] ${context.message}`);
const body = encodeURIComponent(
[
`**Error Details:**`,
`- Category: ${context.category}`,
`- Severity: ${context.severity}`,
`- Message: ${context.message}`,
context.operation ? `- Operation: ${context.operation}` : '',
context.taskId ? `- Task ID: ${context.taskId}` : '',
``,
`**Context:**`,
'```json',
JSON.stringify(context, null, 2),
'```',
``,
`**Environment:**`,
`- VS Code Version: ${vscode.version}`,
`- Extension Version: ${vscode.extensions.getExtension('Hamster.taskmaster')?.packageJSON.version || 'Unknown'}`,
``,
`**Steps to Reproduce:**`,
`1. [Please describe the steps that led to this error]`,
``,
`**Expected Behavior:**`,
`[What should have happened instead]`
]
.filter(Boolean)
.join('\n')
);
return `https://github.com/eyaltoledano/claude-task-master/issues/new?title=${title}&body=${body}`;
}
/**
* Get error by ID
*/
getError(errorId: string): ErrorContext | undefined {
return this.errorLog.get(errorId);
}
/**
* Clear old errors (keep last 100)
*/
clearOldErrors(): void {
if (this.errorLog.size > 100) {
const entriesToKeep = Array.from(this.errorLog.entries()).slice(-100);
this.errorLog.clear();
entriesToKeep.forEach(([id, error]) => this.errorLog.set(id, error));
}
}
}
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/scope-adjustment.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* Tests for scope-adjustment.js module
*/
import { jest } from '@jest/globals';
// Mock dependencies using unstable_mockModule for ES modules
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
log: jest.fn(),
readJSON: jest.fn(),
writeJSON: jest.fn(),
getCurrentTag: jest.fn(() => 'master'),
readComplexityReport: jest.fn(),
findTaskInComplexityReport: jest.fn(),
findProjectRoot: jest.fn()
}));
jest.unstable_mockModule(
'../../../../../scripts/modules/ai-services-unified.js',
() => ({
generateObjectService: jest.fn(),
generateTextService: jest.fn()
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/task-manager.js',
() => ({
findTaskById: jest.fn(),
taskExists: jest.fn()
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/task-manager/analyze-task-complexity.js',
() => ({
default: jest.fn()
})
);
jest.unstable_mockModule('../../../../../src/utils/path-utils.js', () => ({
findComplexityReportPath: jest.fn()
}));
// Import modules after mocking
const {
log,
readJSON,
writeJSON,
readComplexityReport,
findTaskInComplexityReport
} = await import('../../../../../scripts/modules/utils.js');
const { generateObjectService } = await import(
'../../../../../scripts/modules/ai-services-unified.js'
);
const { findTaskById, taskExists } = await import(
'../../../../../scripts/modules/task-manager.js'
);
const { scopeUpTask, scopeDownTask, validateStrength } = await import(
'../../../../../scripts/modules/task-manager/scope-adjustment.js'
);
describe('Scope Adjustment Commands', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('scopeUpTask', () => {
it('should increase task complexity with regular strength', async () => {
// Mock existing task data
const mockTasksData = {
tasks: [
{
id: 1,
title: 'Simple Task',
description: 'Basic description',
details: 'Basic implementation details',
status: 'pending'
}
]
};
const mockTask = {
id: 1,
title: 'Simple Task',
description: 'Basic description',
details: 'Basic implementation details',
status: 'pending'
};
readJSON.mockReturnValue(mockTasksData);
taskExists.mockReturnValue(true);
findTaskById.mockReturnValue({ task: mockTask });
generateObjectService.mockResolvedValue({
mainResult: {
title: 'Complex Task with Advanced Features',
description: 'Enhanced description with more requirements',
details:
'Detailed implementation with error handling, validation, and advanced features',
testStrategy:
'Comprehensive testing including unit, integration, and edge cases'
},
telemetryData: { tokens: 100, cost: 0.01 }
});
const context = {
projectRoot: '/test/project',
tag: 'master',
commandName: 'scope-up',
outputType: 'cli'
};
const result = await scopeUpTask(
'/test/tasks.json',
[1],
'regular',
null, // no custom prompt
context,
'text'
);
expect(result).toBeDefined();
expect(result.updatedTasks).toHaveLength(1);
expect(result.telemetryData).toBeDefined();
expect(writeJSON).toHaveBeenCalledWith(
'/test/tasks.json',
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 1,
title: 'Complex Task with Advanced Features'
})
])
}),
'/test/project',
'master'
);
});
it('should handle custom prompts for targeted scope adjustments', async () => {
const mockTasksData = {
tasks: [
{
id: 1,
title: 'Simple Task',
description: 'Basic description',
details: 'Basic implementation details',
status: 'pending'
}
]
};
const mockTask = {
id: 1,
title: 'Simple Task',
description: 'Basic description',
details: 'Basic implementation details',
status: 'pending'
};
readJSON.mockReturnValue(mockTasksData);
taskExists.mockReturnValue(true);
findTaskById.mockReturnValue({ task: mockTask });
generateObjectService.mockResolvedValue({
mainResult: {
title: 'Task with Enhanced Security',
description: 'Description with security considerations',
details: 'Implementation with security validation and encryption',
testStrategy: 'Security-focused testing strategy'
},
telemetryData: { tokens: 120, cost: 0.012 }
});
const context = {
projectRoot: '/test/project',
tag: 'master',
commandName: 'scope-up',
outputType: 'cli'
};
const customPrompt = 'Focus on adding security features and validation';
const result = await scopeUpTask(
'/test/tasks.json',
[1],
'heavy',
customPrompt,
context,
'text'
);
expect(result).toBeDefined();
expect(generateObjectService).toHaveBeenCalledWith(
expect.objectContaining({
prompt: expect.stringContaining(
'Focus on adding security features and validation'
)
})
);
});
});
describe('scopeDownTask', () => {
it('should decrease task complexity with regular strength', async () => {
const mockTasksData = {
tasks: [
{
id: 1,
title: 'Complex Task with Many Features',
description: 'Comprehensive description with multiple requirements',
details:
'Detailed implementation with advanced features, error handling, validation',
status: 'pending'
}
]
};
const mockTask = {
id: 1,
title: 'Complex Task with Many Features',
description: 'Comprehensive description with multiple requirements',
details:
'Detailed implementation with advanced features, error handling, validation',
status: 'pending'
};
readJSON.mockReturnValue(mockTasksData);
taskExists.mockReturnValue(true);
findTaskById.mockReturnValue({ task: mockTask });
generateObjectService.mockResolvedValue({
mainResult: {
title: 'Simple Task',
description: 'Basic description',
details: 'Basic implementation focusing on core functionality',
testStrategy: 'Simple unit tests for core functionality'
},
telemetryData: { tokens: 80, cost: 0.008 }
});
const context = {
projectRoot: '/test/project',
tag: 'master',
commandName: 'scope-down',
outputType: 'cli'
};
const result = await scopeDownTask(
'/test/tasks.json',
[1],
'regular',
null,
context,
'text'
);
expect(result).toBeDefined();
expect(result.updatedTasks).toHaveLength(1);
expect(writeJSON).toHaveBeenCalled();
});
});
describe('strength level validation', () => {
it('should validate strength parameter correctly', () => {
expect(validateStrength('light')).toBe(true);
expect(validateStrength('regular')).toBe(true);
expect(validateStrength('heavy')).toBe(true);
expect(validateStrength('invalid')).toBe(false);
expect(validateStrength('')).toBe(false);
expect(validateStrength(null)).toBe(false);
});
});
describe('multiple task IDs handling', () => {
it('should handle comma-separated task IDs', async () => {
const mockTasksData = {
tasks: [
{
id: 1,
title: 'Task 1',
description: 'Desc 1',
details: 'Details 1',
status: 'pending'
},
{
id: 2,
title: 'Task 2',
description: 'Desc 2',
details: 'Details 2',
status: 'pending'
}
]
};
readJSON.mockReturnValue(mockTasksData);
taskExists.mockReturnValue(true);
findTaskById
.mockReturnValueOnce({
task: {
id: 1,
title: 'Task 1',
description: 'Desc 1',
details: 'Details 1',
status: 'pending'
}
})
.mockReturnValueOnce({
task: {
id: 2,
title: 'Task 2',
description: 'Desc 2',
details: 'Details 2',
status: 'pending'
}
});
generateObjectService.mockResolvedValue({
mainResult: {
title: 'Enhanced Task',
description: 'Enhanced description',
details: 'Enhanced details',
testStrategy: 'Enhanced testing'
},
telemetryData: { tokens: 100, cost: 0.01 }
});
const context = {
projectRoot: '/test/project',
tag: 'master',
commandName: 'scope-up',
outputType: 'cli'
};
const result = await scopeUpTask(
'/test/tasks.json',
[1, 2],
'regular',
null,
context,
'text'
);
expect(result.updatedTasks).toHaveLength(2);
expect(generateObjectService).toHaveBeenCalledTimes(2);
});
});
});
```
--------------------------------------------------------------------------------
/apps/cli/src/commands/show.command.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview ShowCommand using Commander's native class pattern
* Extends Commander.Command for better integration with the framework
*/
import { type Task, type TmCore, createTmCore } from '@tm/core';
import type { StorageType, Subtask } from '@tm/core';
import boxen from 'boxen';
import chalk from 'chalk';
import { Command } from 'commander';
import { displayTaskDetails } from '../ui/components/task-detail.component.js';
import { displayCommandHeader } from '../utils/display-helpers.js';
import { displayError } from '../utils/error-handler.js';
import * as ui from '../utils/ui.js';
import { getProjectRoot } from '../utils/project-root.js';
/**
* Options interface for the show command
*/
export interface ShowCommandOptions {
id?: string;
status?: string;
format?: 'text' | 'json';
json?: boolean;
silent?: boolean;
project?: string;
}
/**
* Result type from show command
*/
export interface ShowTaskResult {
task: Task | Subtask | null;
found: boolean;
storageType: Exclude<StorageType, 'auto'>;
originalTaskId?: string; // The original task ID requested (for subtasks like "104.1")
}
/**
* Result type for multiple tasks
*/
export interface ShowMultipleTasksResult {
tasks: (Task | Subtask)[];
notFound: string[];
storageType: Exclude<StorageType, 'auto'>;
}
/**
* ShowCommand extending Commander's Command class
* This is a thin presentation layer over @tm/core
*/
export class ShowCommand extends Command {
private tmCore?: TmCore;
private lastResult?: ShowTaskResult | ShowMultipleTasksResult;
constructor(name?: string) {
super(name || 'show');
// Configure the command
this.description('Display detailed information about one or more tasks')
.argument('[id]', 'Task ID(s) to show (comma-separated for multiple)')
.option(
'-i, --id <id>',
'Task ID(s) to show (comma-separated for multiple)'
)
.option('-s, --status <status>', 'Filter subtasks by status')
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.option('--json', 'Output in JSON format (shorthand for --format json)')
.option('--silent', 'Suppress output (useful for programmatic usage)')
.option(
'-p, --project <path>',
'Project root directory (auto-detected if not provided)'
)
.action(
async (taskId: string | undefined, options: ShowCommandOptions) => {
await this.executeCommand(taskId, options);
}
);
}
/**
* Execute the show command
*/
private async executeCommand(
taskId: string | undefined,
options: ShowCommandOptions
): Promise<void> {
try {
// Validate options
if (!this.validateOptions(options)) {
process.exit(1);
}
// Initialize tm-core
await this.initializeCore(getProjectRoot(options.project));
// Get the task ID from argument or option
const idArg = taskId || options.id;
if (!idArg) {
console.error(chalk.red('Error: Please provide a task ID'));
process.exit(1);
}
// Check if multiple IDs are provided (comma-separated)
const taskIds = idArg
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0);
// Get tasks from core
const result =
taskIds.length > 1
? await this.getMultipleTasks(taskIds, options)
: await this.getSingleTask(taskIds[0], options);
// Store result for programmatic access
this.setLastResult(result);
// Display results
if (!options.silent) {
this.displayResults(result, options);
}
} catch (error: any) {
displayError(error);
}
}
/**
* Validate command options
*/
private validateOptions(options: ShowCommandOptions): boolean {
// Validate format
if (options.format && !['text', 'json'].includes(options.format)) {
console.error(chalk.red(`Invalid format: ${options.format}`));
console.error(chalk.gray(`Valid formats: text, json`));
return false;
}
return true;
}
/**
* Initialize TmCore
*/
private async initializeCore(projectRoot: string): Promise<void> {
if (!this.tmCore) {
this.tmCore = await createTmCore({ projectPath: projectRoot });
}
}
/**
* Get a single task from tm-core
*/
private async getSingleTask(
taskId: string,
_options: ShowCommandOptions
): Promise<ShowTaskResult> {
if (!this.tmCore) {
throw new Error('TmCore not initialized');
}
// Get the task
const result = await this.tmCore.tasks.get(taskId);
// Get storage type
const storageType = this.tmCore.tasks.getStorageType();
return {
task: result.task,
found: result.task !== null,
storageType: storageType as Exclude<StorageType, 'auto'>,
originalTaskId: result.isSubtask ? taskId : undefined
};
}
/**
* Get multiple tasks from tm-core
*/
private async getMultipleTasks(
taskIds: string[],
_options: ShowCommandOptions
): Promise<ShowMultipleTasksResult> {
if (!this.tmCore) {
throw new Error('TmCore not initialized');
}
const tasks: (Task | Subtask)[] = [];
const notFound: string[] = [];
// Get each task individually
for (const taskId of taskIds) {
const result = await this.tmCore.tasks.get(taskId);
if (result.task) {
tasks.push(result.task);
} else {
notFound.push(taskId);
}
}
// Get storage type (resolved, not config value)
const storageType = this.tmCore.tasks.getStorageType();
return {
tasks,
notFound,
storageType
};
}
/**
* Display results based on format
*/
private displayResults(
result: ShowTaskResult | ShowMultipleTasksResult,
options: ShowCommandOptions
): void {
// If --json flag is set, override format to 'json'
const format = options.json ? 'json' : options.format || 'text';
switch (format) {
case 'json':
this.displayJson(result);
break;
case 'text':
default:
if ('task' in result) {
// Single task result
this.displaySingleTask(result, options);
} else {
// Multiple tasks result
this.displayMultipleTasks(result, options);
}
break;
}
}
/**
* Display in JSON format
*/
private displayJson(result: ShowTaskResult | ShowMultipleTasksResult): void {
console.log(JSON.stringify(result, null, 2));
}
/**
* Display a single task in text format
*/
private displaySingleTask(
result: ShowTaskResult,
options: ShowCommandOptions
): void {
if (!result.found || !result.task) {
console.log(
boxen(chalk.yellow(`Task not found!`), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'yellow',
borderStyle: 'round',
margin: { top: 1 }
})
);
return;
}
// Display header with storage info
const activeTag = this.tmCore?.config.getActiveTag() || 'master';
displayCommandHeader(this.tmCore, {
tag: activeTag,
storageType: result.storageType
});
console.log(); // Add spacing
// Use the global task details display function
// Pass the original requested ID if it's a subtask
displayTaskDetails(result.task, {
statusFilter: options.status,
showSuggestedActions: true,
originalTaskId: result.originalTaskId,
storageType: result.storageType
});
}
/**
* Display multiple tasks in text format
*/
private displayMultipleTasks(
result: ShowMultipleTasksResult,
_options: ShowCommandOptions
): void {
// Display header with storage info
const activeTag = this.tmCore?.config.getActiveTag() || 'master';
displayCommandHeader(this.tmCore, {
tag: activeTag,
storageType: result.storageType
});
if (result.notFound.length > 0) {
console.log(chalk.yellow(`\n⚠️ Not found: ${result.notFound.join(', ')}`));
}
if (result.tasks.length === 0) {
ui.displayWarning('No tasks found matching the criteria.');
return;
}
// Task table
console.log(chalk.blue.bold(`\n📋 Tasks:\n`));
console.log(
ui.createTaskTable(result.tasks, {
showSubtasks: true,
showDependencies: true
})
);
}
/**
* Set the last result for programmatic access
*/
private setLastResult(
result: ShowTaskResult | ShowMultipleTasksResult
): void {
this.lastResult = result;
}
/**
* Get the last result (for programmatic usage)
*/
getLastResult(): ShowTaskResult | ShowMultipleTasksResult | undefined {
return this.lastResult;
}
/**
* Clean up resources
*/
async cleanup(): Promise<void> {
if (this.tmCore) {
this.tmCore = undefined;
}
}
/**
* Register this command on an existing program
*/
static register(program: Command, name?: string): ShowCommand {
const showCommand = new ShowCommand(name);
program.addCommand(showCommand);
return showCommand;
}
}
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/remove-subtask.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* Tests for the removeSubtask function
*/
import { jest } from '@jest/globals';
import path from 'path';
// Mock dependencies
const mockReadJSON = jest.fn();
const mockWriteJSON = jest.fn();
const mockGenerateTaskFiles = jest.fn();
// Mock path module
jest.mock('path', () => ({
dirname: jest.fn()
}));
// Define test version of the removeSubtask function
const testRemoveSubtask = (
tasksPath,
subtaskId,
convertToTask = false,
generateFiles = true,
context = { tag: 'master' }
) => {
const { projectRoot = undefined, tag = 'master' } = context;
// Read the existing tasks
const data = mockReadJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks) {
throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
}
// Parse the subtask ID (format: "parentId.subtaskId")
if (!subtaskId.includes('.')) {
throw new Error(`Invalid subtask ID format: ${subtaskId}`);
}
const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
const parentId = parseInt(parentIdStr, 10);
const subtaskIdNum = parseInt(subtaskIdStr, 10);
// Find the parent task
const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) {
throw new Error(`Parent task with ID ${parentId} not found`);
}
// Check if parent has subtasks
if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
throw new Error(`Parent task ${parentId} has no subtasks`);
}
// Find the subtask to remove
const subtaskIndex = parentTask.subtasks.findIndex(
(st) => st.id === subtaskIdNum
);
if (subtaskIndex === -1) {
throw new Error(`Subtask ${subtaskId} not found`);
}
// Get a copy of the subtask before removing it
const removedSubtask = { ...parentTask.subtasks[subtaskIndex] };
// Remove the subtask from the parent
parentTask.subtasks.splice(subtaskIndex, 1);
// If parent has no more subtasks, remove the subtasks array
if (parentTask.subtasks.length === 0) {
delete parentTask.subtasks;
}
let convertedTask = null;
// Convert the subtask to a standalone task if requested
if (convertToTask) {
// Find the highest task ID to determine the next ID
const highestId = Math.max(...data.tasks.map((t) => t.id));
const newTaskId = highestId + 1;
// Create the new task from the subtask
convertedTask = {
id: newTaskId,
title: removedSubtask.title,
description: removedSubtask.description || '',
details: removedSubtask.details || '',
status: removedSubtask.status || 'pending',
dependencies: removedSubtask.dependencies || [],
priority: parentTask.priority || 'medium' // Inherit priority from parent
};
// Add the parent task as a dependency if not already present
if (!convertedTask.dependencies.includes(parentId)) {
convertedTask.dependencies.push(parentId);
}
// Add the converted task to the tasks array
data.tasks.push(convertedTask);
}
// Write the updated tasks back to the file
mockWriteJSON(tasksPath, data, projectRoot, tag);
// Generate task files if requested
if (generateFiles) {
mockGenerateTaskFiles(tasksPath, path.dirname(tasksPath));
}
return convertedTask;
};
describe('removeSubtask function', () => {
// Reset mocks before each test
beforeEach(() => {
jest.clearAllMocks();
// Default mock implementations
mockReadJSON.mockImplementation((p, root, tag) => {
expect(tag).toBeDefined();
expect(tag).toBe('master');
return {
tasks: [
{
id: 1,
title: 'Parent Task',
description: 'This is a parent task',
status: 'pending',
dependencies: [],
subtasks: [
{
id: 1,
title: 'Subtask 1',
description: 'This is subtask 1',
status: 'pending',
dependencies: [],
parentTaskId: 1
},
{
id: 2,
title: 'Subtask 2',
description: 'This is subtask 2',
status: 'in-progress',
dependencies: [1], // Depends on subtask 1
parentTaskId: 1
}
]
},
{
id: 2,
title: 'Another Task',
description: 'This is another task',
status: 'pending',
dependencies: [1]
}
]
};
});
// Setup success write response
mockWriteJSON.mockImplementation((path, data, root, tag) => {
expect(tag).toBe('master');
return data;
});
});
test('should remove a subtask from its parent task', async () => {
// Execute the test version of removeSubtask to remove subtask 1.1
testRemoveSubtask('tasks/tasks.json', '1.1', false, true, {
tag: 'master'
});
// Verify readJSON was called with the correct path
expect(mockReadJSON).toHaveBeenCalledWith(
'tasks/tasks.json',
undefined,
'master'
);
// Verify writeJSON was called with updated data
expect(mockWriteJSON).toHaveBeenCalled();
// Verify generateTaskFiles was called
// expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should convert a subtask to a standalone task', async () => {
// Execute the test version of removeSubtask to convert subtask 1.1 to a standalone task
const result = testRemoveSubtask('tasks/tasks.json', '1.1', true, true, {
tag: 'master'
});
// Verify the result is the new task
expect(result).toBeDefined();
expect(result.id).toBe(3);
expect(result.title).toBe('Subtask 1');
expect(result.dependencies).toContain(1);
// Verify writeJSON was called
expect(mockWriteJSON).toHaveBeenCalled();
// Verify generateTaskFiles was called
// expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should throw an error if subtask ID format is invalid', async () => {
// Expect an error for invalid subtask ID format
expect(() =>
testRemoveSubtask('tasks/tasks.json', '1', false, true, { tag: 'master' })
).toThrow(/Invalid subtask ID format/);
// Verify writeJSON was not called
expect(mockWriteJSON).not.toHaveBeenCalled();
});
test('should throw an error if parent task does not exist', async () => {
// Expect an error for non-existent parent task
expect(() =>
testRemoveSubtask('tasks/tasks.json', '999.1', false, true, {
tag: 'master'
})
).toThrow(/Parent task with ID 999 not found/);
// Verify writeJSON was not called
expect(mockWriteJSON).not.toHaveBeenCalled();
});
test('should throw an error if subtask does not exist', async () => {
// Expect an error for non-existent subtask
expect(() =>
testRemoveSubtask('tasks/tasks.json', '1.999', false, true, {
tag: 'master'
})
).toThrow(/Subtask 1.999 not found/);
// Verify writeJSON was not called
expect(mockWriteJSON).not.toHaveBeenCalled();
});
test('should remove subtasks array if last subtask is removed', async () => {
// Create a data object with just one subtask
mockReadJSON.mockImplementationOnce((p, root, tag) => {
expect(tag).toBe('master');
return {
tasks: [
{
id: 1,
title: 'Parent Task',
description: 'This is a parent task',
status: 'pending',
dependencies: [],
subtasks: [
{
id: 1,
title: 'Last Subtask',
description: 'This is the last subtask',
status: 'pending',
dependencies: [],
parentTaskId: 1
}
]
},
{
id: 2,
title: 'Another Task',
description: 'This is another task',
status: 'pending',
dependencies: [1]
}
]
};
});
// Mock the behavior of writeJSON to capture the updated tasks data
const updatedTasksData = { tasks: [] };
mockWriteJSON.mockImplementation((path, data, root, tag) => {
expect(tag).toBe('master');
// Store the data for assertions
updatedTasksData.tasks = [...data.tasks];
return data;
});
// Remove the last subtask
testRemoveSubtask('tasks/tasks.json', '1.1', false, true, {
tag: 'master'
});
// Verify writeJSON was called
expect(mockWriteJSON).toHaveBeenCalled();
// Verify the subtasks array was removed completely
const parentTask = updatedTasksData.tasks.find((t) => t.id === 1);
expect(parentTask).toBeDefined();
expect(parentTask.subtasks).toBeUndefined();
// Verify generateTaskFiles was called
// expect(mockGenerateTaskFiles).toHaveBeenCalled();
});
test('should not regenerate task files if generateFiles is false', async () => {
// Execute the test version of removeSubtask with generateFiles = false
testRemoveSubtask('tasks/tasks.json', '1.1', false, false, {
tag: 'master'
});
// Verify writeJSON was called
expect(mockWriteJSON).toHaveBeenCalled();
// Verify task files were not regenerated
expect(mockGenerateTaskFiles).not.toHaveBeenCalled();
});
});
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/storage/services/storage-factory.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Storage factory for creating appropriate storage implementations
*/
import {
ERROR_CODES,
TaskMasterError
} from '../../../common/errors/task-master-error.js';
import type {
IConfiguration,
RuntimeStorageConfig,
StorageSettings
} from '../../../common/interfaces/configuration.interface.js';
import type { IStorage } from '../../../common/interfaces/storage.interface.js';
import { getLogger } from '../../../common/logger/index.js';
import { AuthManager } from '../../auth/managers/auth-manager.js';
import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js';
import { ApiStorage } from '../adapters/api-storage.js';
import { FileStorage } from '../adapters/file-storage/index.js';
/**
* Factory for creating storage implementations based on configuration
*/
export class StorageFactory {
/**
* Create a storage implementation from runtime storage config
* This is the preferred method when you have a RuntimeStorageConfig
* @param storageConfig - Runtime storage configuration
* @param projectPath - Project root path (for file storage)
* @returns Storage implementation
*/
static async createFromStorageConfig(
storageConfig: RuntimeStorageConfig,
projectPath: string
): Promise<IStorage> {
// Wrap the storage config in the expected format, including projectPath
// This ensures ApiStorage receives the projectPath for projectId
return StorageFactory.create(
{ storage: storageConfig, projectPath } as Partial<IConfiguration>,
projectPath
);
}
/**
* Create a storage implementation based on configuration
* @param config - Configuration object
* @param projectPath - Project root path (for file storage)
* @returns Storage implementation
*/
static async create(
config: Partial<IConfiguration>,
projectPath: string
): Promise<IStorage> {
const storageType = config.storage?.type || 'auto';
const logger = getLogger('StorageFactory');
switch (storageType) {
case 'file':
logger.debug('📁 Using local file storage');
return StorageFactory.createFileStorage(projectPath, config);
case 'api':
if (!StorageFactory.isHamsterAvailable(config)) {
const missing: string[] = [];
if (!config.storage?.apiEndpoint) missing.push('apiEndpoint');
if (!config.storage?.apiAccessToken) missing.push('apiAccessToken');
// Check if authenticated via AuthManager
const authManager = AuthManager.getInstance();
const hasSession = await authManager.hasValidSession();
if (!hasSession) {
throw new TaskMasterError(
`API storage not fully configured (${missing.join(', ') || 'credentials missing'}). Run: tm auth login, or set the missing field(s).`,
ERROR_CODES.MISSING_CONFIGURATION,
{ storageType: 'api', missing }
);
}
// Use auth token from AuthManager
const accessToken = await authManager.getAccessToken();
if (accessToken) {
// Merge with existing storage config, ensuring required fields
const nextStorage: StorageSettings = {
...(config.storage as StorageSettings),
type: 'api',
apiAccessToken: accessToken,
apiEndpoint:
config.storage?.apiEndpoint ||
process.env.TM_BASE_DOMAIN ||
process.env.TM_PUBLIC_BASE_DOMAIN ||
'https://tryhamster.com/api'
};
// Validate that apiEndpoint is defined
if (!nextStorage.apiEndpoint) {
throw new TaskMasterError(
'API endpoint could not be determined.',
ERROR_CODES.MISSING_CONFIGURATION,
{ storageType: 'api' }
);
}
config.storage = nextStorage;
}
}
logger.info('☁️ Using API storage');
return StorageFactory.createApiStorage(config);
case 'auto':
// Auto-detect based on authentication status
const authManager = AuthManager.getInstance();
// First check if API credentials are explicitly configured
if (StorageFactory.isHamsterAvailable(config)) {
logger.info('☁️ Using API storage (configured)');
return StorageFactory.createApiStorage(config);
}
// Then check if authenticated via Supabase
const hasSession = await authManager.hasValidSession();
if (hasSession) {
const accessToken = await authManager.getAccessToken();
const context = authManager.getContext();
// Validate we have the necessary context for API storage
if (!context?.briefId) {
logger.debug(
'📁 User authenticated but no brief selected, using file storage'
);
return StorageFactory.createFileStorage(projectPath, config);
}
if (accessToken) {
// Configure API storage with Supabase session token
const nextStorage: StorageSettings = {
...(config.storage as StorageSettings),
type: 'api',
apiAccessToken: accessToken,
apiEndpoint:
config.storage?.apiEndpoint ||
process.env.TM_BASE_DOMAIN ||
process.env.TM_PUBLIC_BASE_DOMAIN ||
'https://tryhamster.com/api'
};
config.storage = nextStorage;
logger.info('☁️ Using API storage (authenticated)');
return StorageFactory.createApiStorage(config);
}
}
// Default to file storage
logger.debug('📁 Using local file storage');
return StorageFactory.createFileStorage(projectPath, config);
default:
throw new TaskMasterError(
`Unknown storage type: ${storageType}`,
ERROR_CODES.INVALID_INPUT,
{ storageType }
);
}
}
/**
* Create file storage implementation
*/
private static createFileStorage(
projectPath: string,
config: Partial<IConfiguration>
): FileStorage {
const basePath = config.storage?.basePath || projectPath;
return new FileStorage(basePath);
}
/**
* Create API storage implementation
*/
private static createApiStorage(config: Partial<IConfiguration>): ApiStorage {
// Use our SupabaseAuthClient instead of creating a raw Supabase client
const supabaseAuthClient = new SupabaseAuthClient();
const supabaseClient = supabaseAuthClient.getClient();
return new ApiStorage({
supabaseClient,
projectId: config.projectPath || '',
enableRetry: config.retry?.retryOnNetworkError,
maxRetries: config.retry?.retryAttempts
});
}
/**
* Detect optimal storage type based on available configuration
*/
static detectOptimalStorage(config: Partial<IConfiguration>): 'file' | 'api' {
// If API credentials are provided, prefer API storage (Hamster)
if (config.storage?.apiEndpoint && config.storage?.apiAccessToken) {
return 'api';
}
// Default to file storage
return 'file';
}
/**
* Validate storage configuration
*/
static validateStorageConfig(config: Partial<IConfiguration>): {
isValid: boolean;
errors: string[];
} {
const errors: string[] = [];
const storageType = config.storage?.type;
if (!storageType) {
errors.push('Storage type is not specified');
return { isValid: false, errors };
}
switch (storageType) {
case 'api':
if (!config.storage?.apiEndpoint) {
errors.push('API endpoint is required for API storage');
}
if (!config.storage?.apiAccessToken) {
errors.push('API access token is required for API storage');
}
break;
case 'file':
// File storage doesn't require additional config
break;
case 'auto':
// Auto storage is valid - it will determine the actual type at runtime
// No specific validation needed as it will fall back to file if API not configured
break;
default:
errors.push(`Unknown storage type: ${storageType}`);
}
return {
isValid: errors.length === 0,
errors
};
}
/**
* Check if Hamster (API storage) is available
*/
static isHamsterAvailable(config: Partial<IConfiguration>): boolean {
return !!(config.storage?.apiEndpoint && config.storage?.apiAccessToken);
}
/**
* Create a storage implementation with fallback
* Tries API storage first, falls back to file storage
*/
static async createWithFallback(
config: Partial<IConfiguration>,
projectPath: string
): Promise<IStorage> {
// Try API storage if configured
if (StorageFactory.isHamsterAvailable(config)) {
try {
const apiStorage = StorageFactory.createApiStorage(config);
await apiStorage.initialize();
return apiStorage;
} catch (error) {
const logger = getLogger('StorageFactory');
logger.warn(
'Failed to initialize API storage, falling back to file storage:',
error
);
}
}
// Fallback to file storage
return StorageFactory.createFileStorage(projectPath, config);
}
}
```
--------------------------------------------------------------------------------
/apps/extension/src/components/ConfigView.tsx:
--------------------------------------------------------------------------------
```typescript
import { ArrowLeft, RefreshCw, Settings } from 'lucide-react';
import type React from 'react';
import { useEffect, useState, useCallback } from 'react';
import { Badge } from './ui/badge';
import { Button } from './ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from './ui/card';
import { ScrollArea } from './ui/scroll-area';
import { Separator } from './ui/separator';
interface ModelConfig {
provider: string;
modelId: string;
maxTokens: number;
temperature: number;
}
interface ConfigData {
models?: {
main?: ModelConfig;
research?: ModelConfig;
fallback?: ModelConfig;
};
global?: {
defaultNumTasks?: number;
defaultSubtasks?: number;
defaultPriority?: string;
projectName?: string;
responseLanguage?: string;
};
}
interface ConfigViewProps {
sendMessage: (message: any) => Promise<any>;
onNavigateBack: () => void;
}
export const ConfigView: React.FC<ConfigViewProps> = ({
sendMessage,
onNavigateBack
}) => {
const [config, setConfig] = useState<ConfigData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const loadConfig = useCallback(async () => {
setLoading(true);
setError(null);
try {
const response = await sendMessage({ type: 'getConfig' });
setConfig(response);
} catch (err) {
setError('Failed to load configuration');
console.error('Error loading config:', err);
} finally {
setLoading(false);
}
}, [sendMessage]);
useEffect(() => {
loadConfig();
}, [loadConfig]);
const modelLabels = {
main: {
label: 'Main Model',
icon: '🤖',
description: 'Primary model for task generation'
},
research: {
label: 'Research Model',
icon: '🔍',
description: 'Model for research-backed operations'
},
fallback: {
label: 'Fallback Model',
icon: '🔄',
description: 'Backup model if primary fails'
}
};
return (
<div className="flex flex-col h-full bg-vscode-editor-background">
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-vscode-border">
<div className="flex items-center gap-3">
<Button
variant="ghost"
size="icon"
onClick={onNavigateBack}
className="h-8 w-8"
>
<ArrowLeft className="h-4 w-4" />
</Button>
<div className="flex items-center gap-2">
<Settings className="w-5 h-5" />
<h1 className="text-lg font-semibold">Task Master Configuration</h1>
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={loadConfig}
className="h-8 w-8"
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
{/* Content */}
<ScrollArea className="flex-1 overflow-hidden">
<div className="p-6 pb-12">
{loading ? (
<div className="flex items-center justify-center py-8">
<RefreshCw className="w-6 h-6 animate-spin text-vscode-foreground/50" />
</div>
) : error ? (
<div className="text-red-500 text-center py-8">{error}</div>
) : config ? (
<div className="space-y-6 max-w-4xl mx-auto">
{/* Models Section */}
<Card>
<CardHeader>
<CardTitle>AI Models</CardTitle>
<CardDescription>
Models configured for different Task Master operations
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{config.models &&
Object.entries(config.models).map(([key, modelConfig]) => {
const label =
modelLabels[key as keyof typeof modelLabels];
if (!label || !modelConfig) return null;
return (
<div key={key} className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-lg">{label.icon}</span>
<div>
<h4 className="font-medium">{label.label}</h4>
<p className="text-xs text-vscode-foreground/60">
{label.description}
</p>
</div>
</div>
<div className="bg-vscode-input/20 rounded-md p-3 space-y-1">
<div className="flex justify-between">
<span className="text-sm text-vscode-foreground/80">
Provider:
</span>
<Badge variant="secondary">
{modelConfig.provider}
</Badge>
</div>
<div className="flex justify-between">
<span className="text-sm text-vscode-foreground/80">
Model:
</span>
<code className="text-xs font-mono bg-vscode-input/30 px-2 py-1 rounded">
{modelConfig.modelId}
</code>
</div>
<div className="flex justify-between">
<span className="text-sm text-vscode-foreground/80">
Max Tokens:
</span>
<span className="text-sm">
{modelConfig.maxTokens.toLocaleString()}
</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-vscode-foreground/80">
Temperature:
</span>
<span className="text-sm">
{modelConfig.temperature}
</span>
</div>
</div>
</div>
);
})}
</CardContent>
</Card>
{/* Task Defaults Section */}
{config.global && (
<Card>
<CardHeader>
<CardTitle>Task Defaults</CardTitle>
<CardDescription>
Default values for new tasks and subtasks
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Default Number of Tasks
</span>
<Badge variant="outline">
{config.global.defaultNumTasks || 10}
</Badge>
</div>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Default Number of Subtasks
</span>
<Badge variant="outline">
{config.global.defaultSubtasks || 5}
</Badge>
</div>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Default Priority
</span>
<Badge
variant={
config.global.defaultPriority === 'high'
? 'destructive'
: config.global.defaultPriority === 'low'
? 'secondary'
: 'default'
}
>
{config.global.defaultPriority || 'medium'}
</Badge>
</div>
{config.global.projectName && (
<>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Project Name
</span>
<span className="text-sm text-vscode-foreground/80">
{config.global.projectName}
</span>
</div>
</>
)}
{config.global.responseLanguage && (
<>
<Separator />
<div className="flex justify-between items-center">
<span className="text-sm font-medium">
Response Language
</span>
<span className="text-sm text-vscode-foreground/80">
{config.global.responseLanguage}
</span>
</div>
</>
)}
</div>
</CardContent>
</Card>
)}
{/* Info Card */}
<Card>
<CardContent className="pt-6">
<p className="text-sm text-vscode-foreground/60">
To modify these settings, go to{' '}
<code className="bg-vscode-input/30 px-1 py-0.5 rounded">
.taskmaster/config.json
</code>{' '}
and modify them, or use the MCP.
</p>
</CardContent>
</Card>
</div>
) : (
<div className="text-center py-8 text-vscode-foreground/50">
No configuration found. Please run `task-master init` in your
project.
</div>
)}
</div>
</ScrollArea>
</div>
);
};
```
--------------------------------------------------------------------------------
/src/prompts/expand-task.json:
--------------------------------------------------------------------------------
```json
{
"id": "expand-task",
"version": "1.0.0",
"description": "Break down a task into detailed subtasks",
"metadata": {
"author": "system",
"created": "2024-01-01T00:00:00Z",
"updated": "2024-01-01T00:00:00Z",
"tags": ["expansion", "subtasks", "breakdown"]
},
"parameters": {
"subtaskCount": {
"type": "number",
"required": true,
"description": "Number of subtasks to generate"
},
"task": {
"type": "object",
"required": true,
"description": "The task to expand"
},
"nextSubtaskId": {
"type": "number",
"required": true,
"description": "Starting ID for new subtasks"
},
"useResearch": {
"type": "boolean",
"default": false,
"description": "Use research mode"
},
"expansionPrompt": {
"type": "string",
"required": false,
"description": "Expansion prompt from complexity report"
},
"additionalContext": {
"type": "string",
"required": false,
"default": "",
"description": "Additional context for task expansion"
},
"complexityReasoningContext": {
"type": "string",
"required": false,
"default": "",
"description": "Complexity analysis reasoning context"
},
"gatheredContext": {
"type": "string",
"required": false,
"default": "",
"description": "Gathered project context"
},
"hasCodebaseAnalysis": {
"type": "boolean",
"required": false,
"default": false,
"description": "Whether codebase analysis is available"
},
"projectRoot": {
"type": "string",
"required": false,
"default": "",
"description": "Project root path for context"
}
},
"prompts": {
"complexity-report": {
"condition": "expansionPrompt",
"system": "You are an AI assistant helping with task breakdown. Generate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks based on the provided prompt and context.\n\nIMPORTANT: Your response MUST be a JSON object with a \"subtasks\" property containing an array of subtask objects. Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)\n\nYou may optionally include a \"metadata\" object. Do not include any other top-level properties.",
"user": "Break down the following task:\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}\n\n{{expansionPrompt}}{{#if additionalContext}}\n\n{{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\n\n{{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nGenerate {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} subtasks. CRITICAL: Use sequential IDs starting from {{nextSubtaskId}} (first={{nextSubtaskId}}, second={{nextSubtaskId}}+1, etc.)."
},
"research": {
"condition": "useResearch === true && !expansionPrompt",
"system": "You are an AI assistant with research capabilities analyzing and breaking down software development tasks.\n\nIMPORTANT: Your response MUST be a JSON object with a \"subtasks\" property containing an array of subtask objects. Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)\n\nYou may optionally include a \"metadata\" object. Do not include any other top-level properties.",
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Analyze the following task and break it down into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks. Each subtask should be actionable and well-defined.\n\nParent Task:\nID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nConsider this context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: You MUST use sequential IDs starting from {{nextSubtaskId}}. The first subtask MUST have id={{nextSubtaskId}}, the second MUST have id={{nextSubtaskId}}+1, and so on. Do NOT use parent task ID in subtask numbering!"
},
"default": {
"system": "You are an AI assistant helping with task breakdown for software development. Break down high-level tasks into specific, actionable subtasks that can be implemented sequentially.\n\nIMPORTANT: Your response MUST be a JSON object with a \"subtasks\" property containing an array of subtask objects. Each subtask must include ALL of the following fields:\n- id: MUST be sequential integers starting EXACTLY from {{nextSubtaskId}}. First subtask id={{nextSubtaskId}}, second id={{nextSubtaskId}}+1, etc. DO NOT use any other numbering pattern!\n- title: A clear, actionable title (5-200 characters)\n- description: A detailed description (minimum 10 characters)\n- dependencies: An array of task IDs this subtask depends on (can be empty [])\n- details: Implementation details (minimum 20 characters)\n- status: Must be \"pending\" for new subtasks\n- testStrategy: Testing approach (can be null)\n\nYou may optionally include a \"metadata\" object. Do not include any other top-level properties.",
"user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating subtasks:\n\n1. Use the Glob tool to explore relevant files for this task (e.g., \"**/*.js\", \"src/**/*.ts\")\n2. Use the Grep tool to search for existing implementations related to this task\n3. Use the Read tool to examine files that would be affected by this task\n4. Understand the current implementation state and patterns used\n\nBased on your analysis:\n- Identify existing code that relates to this task\n- Understand patterns and conventions to follow\n- Generate subtasks that integrate smoothly with existing code\n- Ensure subtasks are specific and actionable based on the actual codebase\n\nProject Root: {{projectRoot}}\n\n{{/if}}Break down this task into {{#if (gt subtaskCount 0)}}exactly {{subtaskCount}}{{else}}an appropriate number of{{/if}} specific subtasks:\n\nTask ID: {{task.id}}\nTitle: {{task.title}}\nDescription: {{task.description}}\nCurrent details: {{#if task.details}}{{task.details}}{{else}}None{{/if}}{{#if additionalContext}}\nAdditional context: {{additionalContext}}{{/if}}{{#if complexityReasoningContext}}\nComplexity Analysis Reasoning: {{complexityReasoningContext}}{{/if}}{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}{{/if}}\n\nCRITICAL: You MUST use sequential IDs starting from {{nextSubtaskId}}. The first subtask MUST have id={{nextSubtaskId}}, the second MUST have id={{nextSubtaskId}}+1, and so on. Do NOT use parent task ID in subtask numbering!"
}
}
}
```
--------------------------------------------------------------------------------
/.taskmaster/reports/task-complexity-report_tdd-phase-1-core-rails.json:
--------------------------------------------------------------------------------
```json
{
"meta": {
"generatedAt": "2025-10-09T12:47:27.960Z",
"tasksAnalyzed": 10,
"totalTasks": 10,
"analysisCount": 10,
"thresholdScore": 5,
"projectName": "Taskmaster",
"usedResearch": false
},
"complexityAnalysis": [
{
"taskId": 1,
"taskTitle": "Design and Implement Global Storage System",
"complexityScore": 7,
"recommendedSubtasks": 6,
"expansionPrompt": "Break down the global storage system implementation into: 1) Path normalization utilities with cross-platform support, 2) Run ID generation and validation, 3) Manifest.json structure and management, 4) Activity.jsonl append-only logging, 5) State.json mutable checkpoint handling, and 6) Directory structure creation and cleanup. Focus on robust error handling, atomic operations, and isolation between different runs.",
"reasoning": "Complex system requiring cross-platform path handling, multiple file formats (JSON/JSONL), atomic operations, and state management. The existing codebase shows sophisticated file operations infrastructure but this extends beyond current patterns. Implementation involves filesystem operations, concurrency concerns, and data integrity."
},
{
"taskId": 2,
"taskTitle": "Build GitAdapter with Safety Checks",
"complexityScore": 8,
"recommendedSubtasks": 7,
"expansionPrompt": "Decompose GitAdapter into: 1) Git repository detection and validation, 2) Working tree status checking with detailed reporting, 3) Branch operations (create, checkout, list) with safety guards, 4) Commit operations with metadata embedding, 5) Default branch detection and protection logic, 6) Push operations with conflict handling, and 7) Branch name generation from patterns. Emphasize safety checks, confirmation gates, and comprehensive error messages.",
"reasoning": "High complexity due to git operations safety requirements, multiple git commands integration, error handling for various git states, and safety mechanisms. The PRD emphasizes never allowing commits on default branch and requiring clean working tree - critical safety features that need robust implementation."
},
{
"taskId": 3,
"taskTitle": "Implement Test Result Validator",
"complexityScore": 5,
"recommendedSubtasks": 4,
"expansionPrompt": "Split test validation into: 1) Input validation and schema definition for test results, 2) RED phase validation logic (ensuring failures exist), 3) GREEN phase validation logic (ensuring all tests pass), and 4) Coverage threshold validation with configurable limits. Include comprehensive validation messages and suggestions for common failure scenarios.",
"reasoning": "Moderate complexity focused on business logic validation. The validator is framework-agnostic (only validates reported numbers), has clear validation rules, and well-defined input/output. The existing codebase shows validation patterns that can be leveraged."
},
{
"taskId": 4,
"taskTitle": "Develop WorkflowOrchestrator State Machine",
"complexityScore": 9,
"recommendedSubtasks": 8,
"expansionPrompt": "Structure the orchestrator into: 1) State machine definition and transitions (Preflight → BranchSetup → SubtaskLoop → Finalize), 2) Event emission system with comprehensive event types, 3) State persistence and recovery mechanisms, 4) Phase coordination and validation, 5) Subtask iteration and progress tracking, 6) Error handling and recovery strategies, 7) Resume functionality from checkpoints, and 8) Integration points for Git, Test, and other adapters.",
"reasoning": "Very high complexity as the central coordination component. Must orchestrate multiple adapters, handle state transitions, event emission, persistence, and recovery. The state machine needs to be robust, resumable, and coordinate all other components. Critical for the entire workflow's reliability."
},
{
"taskId": 5,
"taskTitle": "Create Enhanced Commit Message Generator",
"complexityScore": 4,
"recommendedSubtasks": 3,
"expansionPrompt": "Organize commit message generation into: 1) Template parsing and variable substitution with configurable templates, 2) Scope detection from changed files with intelligent categorization, and 3) Metadata embedding (task context, test results, coverage) with conventional commits compliance. Ensure messages are parseable and contain all required task metadata.",
"reasoning": "Relatively straightforward text processing and template system. The conventional commits format is well-defined, and the metadata requirements are clear. The existing package.json shows commander dependency for CLI patterns that can be leveraged."
},
{
"taskId": 6,
"taskTitle": "Implement Subtask TDD Loop",
"complexityScore": 8,
"recommendedSubtasks": 6,
"expansionPrompt": "Break down the TDD loop into: 1) RED phase orchestration with test generation coordination, 2) GREEN phase orchestration with implementation guidance, 3) COMMIT phase with file staging and commit creation, 4) Attempt tracking and maximum retry logic, 5) Phase transition validation and state updates, and 6) Activity logging for all phase transitions. Focus on robust state management and clear error recovery paths.",
"reasoning": "High complexity due to coordinating multiple phases, state transitions, retry logic, and integration with multiple adapters (Git, Test, State). This is the core workflow execution engine requiring careful orchestration and error handling."
},
{
"taskId": 7,
"taskTitle": "Build CLI Commands for AI Agent Orchestration",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "Structure CLI commands into: 1) Command registration and argument parsing setup, 2) `start` and `resume` commands with initialization logic, 3) `next` and `status` commands with JSON output formatting, 4) `complete` command with result validation integration, and 5) `commit` and `abort` commands with git operation coordination. Ensure consistent JSON output for machine parsing and comprehensive error handling.",
"reasoning": "Moderate complexity leveraging existing CLI infrastructure. The codebase shows commander usage patterns and CLI structure. Main complexity is in JSON output formatting, argument validation, and integration with the orchestrator component."
},
{
"taskId": 8,
"taskTitle": "Develop MCP Tools for AI Agent Integration",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "Organize MCP tools into: 1) Tool schema definition and parameter validation, 2) `autopilot_start` and `autopilot_resume` tool implementation, 3) `autopilot_next` and `autopilot_status` tools with context provision, 4) `autopilot_complete_phase` tool with validation integration, and 5) `autopilot_commit` tool with git operations. Ensure parity with CLI functionality and proper error handling.",
"reasoning": "Moderate complexity building on existing MCP infrastructure. The codebase shows extensive MCP tooling patterns. Main work is adapting CLI functionality to MCP interface patterns and ensuring consistent behavior between CLI and MCP interfaces."
},
{
"taskId": 9,
"taskTitle": "Write AI Agent Integration Documentation and Templates",
"complexityScore": 2,
"recommendedSubtasks": 2,
"expansionPrompt": "Structure documentation into: 1) Comprehensive workflow documentation with step-by-step examples, command usage, and integration patterns, and 2) Template creation for CLAUDE.md integration, example prompts, and troubleshooting guides. Focus on clear examples and practical integration guidance.",
"reasoning": "Low complexity documentation task. Requires understanding of the implemented system but primarily involves writing clear instructions and examples. The existing codebase shows good documentation patterns that can be followed."
},
{
"taskId": 10,
"taskTitle": "Implement Configuration System and Project Hygiene",
"complexityScore": 5,
"recommendedSubtasks": 4,
"expansionPrompt": "Structure configuration into: 1) Configuration schema definition with comprehensive validation using ajv, 2) Default configuration setup and loading mechanisms, 3) Gitignore management and project directory hygiene rules, and 4) Configuration validation and error reporting. Ensure configurations are validated on startup and provide clear error messages for invalid settings.",
"reasoning": "Moderate complexity involving schema validation, file operations, and configuration management. The package.json shows ajv dependency is available. Configuration systems require careful validation and user-friendly error reporting, but follow established patterns."
}
]
}
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/auth/services/organization.service.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Organization and Brief management service
* Handles fetching and managing organizations and briefs from the API
*/
import type { SupabaseClient } from '@supabase/supabase-js';
import {
ERROR_CODES,
TaskMasterError
} from '../../../common/errors/task-master-error.js';
import { getLogger } from '../../../common/logger/index.js';
import type { Database } from '../../../common/types/database.types.js';
import type { Brief } from '../../briefs/types.js';
/**
* Organization data structure
*/
export interface Organization {
id: string;
name: string;
slug: string;
}
/**
* Task data structure from the remote database
*/
export interface RemoteTask {
id: string;
briefId: string;
documentId: string;
position: number | null;
subtaskPosition: number | null;
status: string;
createdAt: string;
updatedAt: string;
// Document details from join
document?: {
id: string;
document_name: string;
title: string;
description: string;
};
}
/**
* Service for managing organizations and briefs
*/
export class OrganizationService {
private logger = getLogger('OrganizationService');
constructor(private supabaseClient: SupabaseClient<Database>) {}
/**
* Get all organizations for the authenticated user
*/
async getOrganizations(): Promise<Organization[]> {
try {
// The user is already authenticated via the Authorization header
// Query the user_accounts view/table (filtered by RLS for current user)
const { data, error } = await this.supabaseClient
.from('user_accounts')
.select(`
id,
name,
slug
`);
if (error) {
throw new TaskMasterError(
`Failed to fetch organizations: ${error.message}`,
ERROR_CODES.API_ERROR,
{ operation: 'getOrganizations' },
error
);
}
if (!data || data.length === 0) {
this.logger.debug('No organizations found for user');
return [];
}
// Map to our Organization interface
return data.map((org) => ({
id: org.id ?? '',
name: org.name ?? '',
slug: org.slug ?? org.id ?? '' // Use ID as fallback if slug is null
}));
} catch (error) {
if (error instanceof TaskMasterError) {
throw error;
}
throw new TaskMasterError(
'Failed to fetch organizations',
ERROR_CODES.API_ERROR,
{ operation: 'getOrganizations' },
error as Error
);
}
}
/**
* Get a specific organization by ID
*/
async getOrganization(orgId: string): Promise<Organization | null> {
try {
const { data, error } = await this.supabaseClient
.from('accounts')
.select(`
id,
name,
slug
`)
.eq('id', orgId)
.single();
if (error) {
if (error.code === 'PGRST116') {
// No rows found
return null;
}
throw new TaskMasterError(
`Failed to fetch organization: ${error.message}`,
ERROR_CODES.API_ERROR,
{ operation: 'getOrganization', orgId },
error
);
}
if (!data) {
return null;
}
const accountData =
data as Database['public']['Tables']['accounts']['Row'];
return {
id: accountData.id,
name: accountData.name,
slug: accountData.slug || accountData.id
};
} catch (error) {
if (error instanceof TaskMasterError) {
throw error;
}
throw new TaskMasterError(
'Failed to fetch organization',
ERROR_CODES.API_ERROR,
{ operation: 'getOrganization', orgId },
error as Error
);
}
}
/**
* Get all briefs for a specific organization
*/
async getBriefs(orgId: string): Promise<Brief[]> {
try {
const { data, error } = await this.supabaseClient
.from('brief')
.select(`
id,
account_id,
document_id,
status,
created_at,
updated_at,
tasks(count),
document:document_id (
id,
document_name,
title
)
`)
.eq('account_id', orgId)
.order('updated_at', { ascending: false });
if (error) {
throw new TaskMasterError(
`Failed to fetch briefs: ${error.message}`,
ERROR_CODES.API_ERROR,
{ operation: 'getBriefs', orgId },
error
);
}
if (!data || data.length === 0) {
this.logger.debug(`No briefs found for organization ${orgId}`);
return [];
}
// Map to our Brief interface
return data.map((brief: any) => ({
id: brief.id,
accountId: brief.account_id,
documentId: brief.document_id,
status: brief.status,
createdAt: brief.created_at,
updatedAt: brief.updated_at,
taskCount: Array.isArray(brief.tasks)
? (brief.tasks[0]?.count ?? 0)
: 0,
document: brief.document
? {
id: brief.document.id,
document_name: brief.document.document_name,
title: brief.document.title
}
: undefined
}));
} catch (error) {
if (error instanceof TaskMasterError) {
throw error;
}
throw new TaskMasterError(
'Failed to fetch briefs',
ERROR_CODES.API_ERROR,
{ operation: 'getBriefs', orgId },
error as Error
);
}
}
/**
* Get a specific brief by ID
*/
async getBrief(briefId: string): Promise<Brief | null> {
try {
const { data, error } = await this.supabaseClient
.from('brief')
.select(`
id,
account_id,
document_id,
status,
created_at,
updated_at,
document:document_id (
id,
document_name,
title,
description
)
`)
.eq('id', briefId)
.single();
if (error) {
if (error.code === 'PGRST116') {
// No rows found
return null;
}
throw new TaskMasterError(
`Failed to fetch brief: ${error.message}`,
ERROR_CODES.API_ERROR,
{ operation: 'getBrief', briefId },
error
);
}
if (!data) {
return null;
}
const briefData = data as any;
return {
id: briefData.id,
accountId: briefData.account_id,
documentId: briefData.document_id,
status: briefData.status,
createdAt: briefData.created_at,
updatedAt: briefData.updated_at,
document: briefData.document
? {
id: briefData.document.id,
document_name: briefData.document.document_name,
title: briefData.document.title,
description: briefData.document.description
}
: undefined
};
} catch (error) {
if (error instanceof TaskMasterError) {
throw error;
}
throw new TaskMasterError(
'Failed to fetch brief',
ERROR_CODES.API_ERROR,
{ operation: 'getBrief', briefId },
error as Error
);
}
}
/**
* Validate that a user has access to an organization
*/
async validateOrgAccess(orgId: string): Promise<boolean> {
try {
const org = await this.getOrganization(orgId);
return org !== null;
} catch (error) {
this.logger.error(`Failed to validate org access: ${error}`);
return false;
}
}
/**
* Validate that a user has access to a brief
*/
async validateBriefAccess(briefId: string): Promise<boolean> {
try {
const brief = await this.getBrief(briefId);
return brief !== null;
} catch (error) {
this.logger.error(`Failed to validate brief access: ${error}`);
return false;
}
}
/**
* Get all tasks for a specific brief
*/
async getTasks(briefId: string): Promise<RemoteTask[]> {
try {
const { data, error } = await this.supabaseClient
.from('tasks')
.select(`
*,
document:document_id (
id,
document_name,
title,
description
)
`)
.eq('brief_id', briefId)
.order('position', { ascending: true })
.order('subtask_position', { ascending: true })
.order('created_at', { ascending: true });
if (error) {
throw new TaskMasterError(
`Failed to fetch tasks: ${error.message}`,
ERROR_CODES.API_ERROR,
{ operation: 'getTasks', briefId },
error
);
}
if (!data || data.length === 0) {
this.logger.debug(`No tasks found for brief ${briefId}`);
return [];
}
// Map to our RemoteTask interface
return data.map((task: any) => ({
id: task.id,
briefId: task.brief_id,
documentId: task.document_id,
position: task.position,
subtaskPosition: task.subtask_position,
status: task.status,
createdAt: task.created_at,
updatedAt: task.updated_at,
document: task.document
? {
id: task.document.id,
document_name: task.document.document_name,
title: task.document.title,
description: task.document.description
}
: undefined
}));
} catch (error) {
if (error instanceof TaskMasterError) {
throw error;
}
throw new TaskMasterError(
'Failed to fetch tasks',
ERROR_CODES.API_ERROR,
{ operation: 'getTasks', briefId },
error as Error
);
}
}
}
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/tasks/repositories/supabase/supabase-repository.ts:
--------------------------------------------------------------------------------
```typescript
import { SupabaseClient } from '@supabase/supabase-js';
import { Task } from '../../../../common/types/index.js';
import { Database, Json } from '../../../../common/types/database.types.js';
import { TaskMapper } from '../../../../common/mappers/TaskMapper.js';
import { AuthManager } from '../../../auth/managers/auth-manager.js';
import { DependencyFetcher } from './dependency-fetcher.js';
import {
TaskWithRelations,
TaskDatabaseUpdate
} from '../../../../common/types/repository-types.js';
import { LoadTasksOptions } from '../../../../common/interfaces/storage.interface.js';
import { Brief } from '../task-repository.interface.js';
import { z } from 'zod';
// Zod schema for task status validation
const TaskStatusSchema = z.enum([
'pending',
'in-progress',
'done',
'review',
'deferred',
'cancelled',
'blocked'
]);
// Zod schema for task updates
const TaskUpdateSchema = z
.object({
title: z.string().min(1).optional(),
description: z.string().optional(),
status: TaskStatusSchema.optional(),
priority: z.enum(['low', 'medium', 'high', 'critical']).optional(),
details: z.string().optional(),
testStrategy: z.string().optional()
})
.partial();
export class SupabaseRepository {
private dependencyFetcher: DependencyFetcher;
private authManager: AuthManager;
constructor(private supabase: SupabaseClient<Database>) {
this.dependencyFetcher = new DependencyFetcher(supabase);
this.authManager = AuthManager.getInstance();
}
/**
* Gets the current brief ID from auth context
* @throws {Error} If no brief is selected
*/
private getBriefIdOrThrow(): string {
const context = this.authManager.getContext();
if (!context?.briefId) {
throw new Error(
'No brief selected. Please select a brief first using: tm context brief'
);
}
return context.briefId;
}
async getTasks(
_projectId?: string,
options?: LoadTasksOptions
): Promise<Task[]> {
const briefId = this.getBriefIdOrThrow();
// Build query with filters
let query = this.supabase
.from('tasks')
.select(`
*,
document:document_id (
id,
document_name,
title,
description
)
`)
.eq('brief_id', briefId);
// Apply status filter at database level if specified
if (options?.status) {
const dbStatus = this.mapStatusToDatabase(options.status);
query = query.eq('status', dbStatus);
}
// Apply subtask exclusion at database level if specified
if (options?.excludeSubtasks) {
// Only fetch parent tasks (where parent_task_id is null)
query = query.is('parent_task_id', null);
}
// Execute query with ordering
const { data: tasks, error } = await query
.order('position', { ascending: true })
.order('subtask_position', { ascending: true })
.order('created_at', { ascending: true });
if (error) {
throw new Error(`Failed to fetch tasks: ${error.message}`);
}
if (!tasks || tasks.length === 0) {
return [];
}
// Type-safe task ID extraction
const typedTasks = tasks as TaskWithRelations[];
const taskIds = typedTasks.map((t) => t.id);
const dependenciesMap =
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(taskIds);
// Use mapper to convert to internal format
return TaskMapper.mapDatabaseTasksToTasks(tasks, dependenciesMap);
}
async getTask(_projectId: string, taskId: string): Promise<Task | null> {
const briefId = this.getBriefIdOrThrow();
const { data, error } = await this.supabase
.from('tasks')
.select('*')
.eq('brief_id', briefId)
.eq('display_id', taskId.toUpperCase())
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // Not found
}
throw new Error(`Failed to fetch task: ${error.message}`);
}
// Get subtasks if this is a parent task
const { data: subtasksData } = await this.supabase
.from('tasks')
.select('*')
.eq('parent_task_id', data.id)
.order('subtask_position', { ascending: true });
// Get all task IDs (parent + subtasks) to fetch dependencies
const allTaskIds = [data.id, ...(subtasksData?.map((st) => st.id) || [])];
// Fetch dependencies using the dedicated fetcher
const dependenciesByTaskId =
await this.dependencyFetcher.fetchDependenciesWithDisplayIds(allTaskIds);
// Use mapper to convert single task
return TaskMapper.mapDatabaseTaskToTask(
data,
subtasksData || [],
dependenciesByTaskId
);
}
/**
* Get brief information by ID
* Note: This doesn't use getBriefIdOrThrow() because we may need to fetch
* briefs other than the current context brief
*/
async getBrief(briefId: string): Promise<Brief | null> {
const { data, error } = await this.supabase
.from('brief')
.select(`
*,
document:document_id (
id,
title,
description
)
`)
.eq('id', briefId)
.single();
if (error) {
if (error.code === 'PGRST116') {
return null; // Not found
}
throw new Error(`Failed to fetch brief: ${error.message}`);
}
if (!data) {
return null;
}
// Extract document data if available
const document = data.document as any;
// Map database fields to Brief interface
return {
id: data.id,
accountId: data.account_id,
createdAt: data.created_at,
name: document?.title || undefined,
description: document?.description || undefined,
status: data.status || undefined
};
}
async updateTask(
projectId: string,
taskId: string,
updates: Partial<Task>
): Promise<Task> {
const briefId = this.getBriefIdOrThrow();
// Validate updates using Zod schema
try {
TaskUpdateSchema.parse(updates);
} catch (error) {
if (error instanceof z.ZodError) {
const errorMessages = error.issues
.map((err) => `${err.path.join('.')}: ${err.message}`)
.join(', ');
throw new Error(`Invalid task update data: ${errorMessages}`);
}
throw error;
}
// Convert Task fields to database fields with proper typing
const dbUpdates: TaskDatabaseUpdate = {};
if (updates.title !== undefined) dbUpdates.title = updates.title;
if (updates.description !== undefined)
dbUpdates.description = updates.description;
if (updates.status !== undefined)
dbUpdates.status = this.mapStatusToDatabase(updates.status);
if (updates.priority !== undefined)
dbUpdates.priority = this.mapPriorityToDatabase(updates.priority);
// Handle metadata fields (details, testStrategy, etc.)
// Load existing metadata to preserve fields not being updated
const { data: existingMetadataRow, error: existingMetadataError } =
await this.supabase
.from('tasks')
.select('metadata')
.eq('brief_id', briefId)
.eq('display_id', taskId.toUpperCase())
.single();
if (existingMetadataError) {
throw new Error(
`Failed to load existing task metadata: ${existingMetadataError.message}`
);
}
const metadata: Record<string, unknown> = {
...((existingMetadataRow?.metadata as Record<string, unknown>) ?? {})
};
if (updates.details !== undefined) metadata.details = updates.details;
if (updates.testStrategy !== undefined)
metadata.testStrategy = updates.testStrategy;
if (Object.keys(metadata).length > 0) {
dbUpdates.metadata = metadata as Json;
}
// Update the task
const { error } = await this.supabase
.from('tasks')
.update(dbUpdates)
.eq('brief_id', briefId)
.eq('display_id', taskId.toUpperCase());
if (error) {
throw new Error(`Failed to update task: ${error.message}`);
}
// Return the updated task by fetching it
const updatedTask = await this.getTask(projectId, taskId);
if (!updatedTask) {
throw new Error(`Failed to retrieve updated task ${taskId}`);
}
return updatedTask;
}
/**
* Maps internal status to database status
*/
private mapStatusToDatabase(
status: string
): Database['public']['Enums']['task_status'] {
switch (status) {
case 'pending':
return 'todo';
case 'in-progress':
case 'in_progress': // Accept both formats
return 'in_progress';
case 'done':
return 'done';
default:
throw new Error(
`Invalid task status: ${status}. Valid statuses are: pending, in-progress, done`
);
}
}
/**
* Maps internal priority to database priority
* Task Master uses 'critical', database uses 'urgent'
*/
private mapPriorityToDatabase(
priority: string
): Database['public']['Enums']['task_priority'] {
switch (priority) {
case 'critical':
return 'urgent';
case 'low':
case 'medium':
case 'high':
return priority as Database['public']['Enums']['task_priority'];
default:
throw new Error(
`Invalid task priority: ${priority}. Valid priorities are: low, medium, high, critical`
);
}
}
}
```
--------------------------------------------------------------------------------
/src/profiles/claude.js:
--------------------------------------------------------------------------------
```javascript
// Claude Code profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';
// Helper function to recursively copy directory (adopted from Roo profile)
function copyRecursiveSync(src, dest) {
const exists = fs.existsSync(src);
const stats = exists && fs.statSync(src);
const isDirectory = exists && stats.isDirectory();
if (isDirectory) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
fs.readdirSync(src).forEach((childItemName) => {
copyRecursiveSync(
path.join(src, childItemName),
path.join(dest, childItemName)
);
});
} else {
fs.copyFileSync(src, dest);
}
}
// Helper function to recursively remove directory
function removeDirectoryRecursive(dirPath) {
if (fs.existsSync(dirPath)) {
try {
fs.rmSync(dirPath, { recursive: true, force: true });
return true;
} catch (err) {
log('error', `Failed to remove directory ${dirPath}: ${err.message}`);
return false;
}
}
return true;
}
// Lifecycle functions for Claude Code profile
function onAddRulesProfile(targetDir, assetsDir) {
// Note: Commands and agents are now distributed via Claude Code plugin
// Legacy .claude directory copying has been deprecated
log(
'info',
'[Claude] Commands and agents are now available via the Task Master plugin'
);
log('info', '[Claude] Install with: /plugin marketplace add taskmaster');
log('info', '[Claude] Then: /plugin install taskmaster@taskmaster');
// Handle CLAUDE.md import for non-destructive integration
const sourceFile = path.join(assetsDir, 'AGENTS.md');
const userClaudeFile = path.join(targetDir, 'CLAUDE.md');
const taskMasterClaudeFile = path.join(targetDir, '.taskmaster', 'CLAUDE.md');
const importLine = '@./.taskmaster/CLAUDE.md';
const importSection = `\n## Task Master AI Instructions\n**Import Task Master's development workflow commands and guidelines, treat as if import is in the main CLAUDE.md file.**\n${importLine}`;
if (fs.existsSync(sourceFile)) {
try {
// Ensure .taskmaster directory exists
const taskMasterDir = path.join(targetDir, '.taskmaster');
if (!fs.existsSync(taskMasterDir)) {
fs.mkdirSync(taskMasterDir, { recursive: true });
}
// Copy Task Master instructions to .taskmaster/CLAUDE.md
fs.copyFileSync(sourceFile, taskMasterClaudeFile);
log(
'debug',
`[Claude] Created Task Master instructions at ${taskMasterClaudeFile}`
);
// Handle user's CLAUDE.md
if (fs.existsSync(userClaudeFile)) {
// Check if import already exists
const content = fs.readFileSync(userClaudeFile, 'utf8');
if (!content.includes(importLine)) {
// Append import section at the end
const updatedContent = content.trim() + '\n' + importSection + '\n';
fs.writeFileSync(userClaudeFile, updatedContent);
log(
'info',
`[Claude] Added Task Master import to existing ${userClaudeFile}`
);
} else {
log(
'info',
`[Claude] Task Master import already present in ${userClaudeFile}`
);
}
} else {
// Create minimal CLAUDE.md with the import section
const minimalContent = `# Claude Code Instructions\n${importSection}\n`;
fs.writeFileSync(userClaudeFile, minimalContent);
log(
'info',
`[Claude] Created ${userClaudeFile} with Task Master import`
);
}
} catch (err) {
log(
'error',
`[Claude] Failed to set up Claude instructions: ${err.message}`
);
}
}
}
function onRemoveRulesProfile(targetDir) {
// Note: .claude directory (commands/agents) are now managed by Claude Code plugin
// We no longer remove them here - users should uninstall the plugin separately
log(
'info',
'[Claude] To remove Task Master commands/agents, uninstall the plugin with: /plugin uninstall taskmaster'
);
// Clean up CLAUDE.md import
const userClaudeFile = path.join(targetDir, 'CLAUDE.md');
const taskMasterClaudeFile = path.join(targetDir, '.taskmaster', 'CLAUDE.md');
const importLine = '@./.taskmaster/CLAUDE.md';
try {
// Remove Task Master CLAUDE.md from .taskmaster
if (fs.existsSync(taskMasterClaudeFile)) {
fs.rmSync(taskMasterClaudeFile, { force: true });
log('debug', `[Claude] Removed ${taskMasterClaudeFile}`);
}
// Clean up import from user's CLAUDE.md
if (fs.existsSync(userClaudeFile)) {
const content = fs.readFileSync(userClaudeFile, 'utf8');
const lines = content.split('\n');
const filteredLines = [];
let skipNextLines = 0;
// Remove the Task Master section
for (let i = 0; i < lines.length; i++) {
if (skipNextLines > 0) {
skipNextLines--;
continue;
}
// Check if this is the start of our Task Master section
if (lines[i].includes('## Task Master AI Instructions')) {
// Skip this line and the next two lines (bold text and import)
skipNextLines = 2;
continue;
}
// Also remove standalone import lines (for backward compatibility)
if (lines[i].trim() === importLine) {
continue;
}
filteredLines.push(lines[i]);
}
// Join back and clean up excessive newlines
let updatedContent = filteredLines
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
// Check if file only contained our minimal template
if (
updatedContent === '# Claude Code Instructions' ||
updatedContent === ''
) {
// File only contained our import, remove it
fs.rmSync(userClaudeFile, { force: true });
log('debug', `[Claude] Removed empty ${userClaudeFile}`);
} else {
// Write back without the import
fs.writeFileSync(userClaudeFile, updatedContent + '\n');
log(
'debug',
`[Claude] Removed Task Master import from ${userClaudeFile}`
);
}
}
} catch (err) {
log(
'error',
`[Claude] Failed to remove Claude instructions: ${err.message}`
);
}
}
/**
* Transform standard MCP config format to Claude format
* @param {Object} mcpConfig - Standard MCP configuration object
* @returns {Object} - Transformed Claude configuration object
*/
function transformToClaudeFormat(mcpConfig) {
const claudeConfig = {};
// Transform mcpServers to servers (keeping the same structure but adding type)
if (mcpConfig.mcpServers) {
claudeConfig.mcpServers = {};
for (const [serverName, serverConfig] of Object.entries(
mcpConfig.mcpServers
)) {
// Transform server configuration with type as first key
const reorderedServer = {};
// Add type: "stdio" as the first key
reorderedServer.type = 'stdio';
// Then add the rest of the properties in order
if (serverConfig.command) reorderedServer.command = serverConfig.command;
if (serverConfig.args) reorderedServer.args = serverConfig.args;
if (serverConfig.env) reorderedServer.env = serverConfig.env;
// Add any other properties that might exist
Object.keys(serverConfig).forEach((key) => {
if (!['command', 'args', 'env', 'type'].includes(key)) {
reorderedServer[key] = serverConfig[key];
}
});
claudeConfig.mcpServers[serverName] = reorderedServer;
}
}
return claudeConfig;
}
function onPostConvertRulesProfile(targetDir, assetsDir) {
// For Claude, post-convert is the same as add since we don't transform rules
onAddRulesProfile(targetDir, assetsDir);
// Transform MCP configuration to Claude format
const mcpConfigPath = path.join(targetDir, '.mcp.json');
if (fs.existsSync(mcpConfigPath)) {
try {
const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8'));
const claudeConfig = transformToClaudeFormat(mcpConfig);
// Write back the transformed configuration
fs.writeFileSync(
mcpConfigPath,
JSON.stringify(claudeConfig, null, '\t') + '\n'
);
log(
'debug',
`[Claude] Transformed MCP configuration to Claude format at ${mcpConfigPath}`
);
} catch (err) {
log(
'error',
`[Claude] Failed to transform MCP configuration: ${err.message}`
);
}
}
}
// Create and export claude profile using the base factory
export const claudeProfile = createProfile({
name: 'claude',
displayName: 'Claude Code',
url: 'claude.ai',
docsUrl: 'docs.anthropic.com/en/docs/claude-code',
profileDir: '.', // Root directory
rulesDir: '.', // No specific rules directory needed
mcpConfigName: '.mcp.json', // Place MCP config in project root
includeDefaultRules: false,
fileMap: {
'AGENTS.md': '.taskmaster/CLAUDE.md'
},
onAdd: onAddRulesProfile,
onRemove: onRemoveRulesProfile,
onPostConvert: onPostConvertRulesProfile
});
// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };
```
--------------------------------------------------------------------------------
/apps/extension/src/webview/hooks/useTaskQueries.ts:
--------------------------------------------------------------------------------
```typescript
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useVSCodeContext } from '../contexts/VSCodeContext';
import type { TaskMasterTask, TaskUpdates } from '../types';
// Query keys factory
export const taskKeys = {
all: ['tasks'] as const,
lists: () => [...taskKeys.all, 'list'] as const,
list: (filters: { tag?: string; status?: string }) =>
[...taskKeys.lists(), filters] as const,
details: () => [...taskKeys.all, 'detail'] as const,
detail: (id: string) => [...taskKeys.details(), id] as const
};
// Hook to fetch all tasks
export function useTasks(options?: { tag?: string; status?: string }) {
const { sendMessage } = useVSCodeContext();
return useQuery({
queryKey: taskKeys.list(options || {}),
queryFn: async () => {
console.log('🔍 Fetching tasks with options:', options);
const response = await sendMessage({
type: 'getTasks',
data: {
tag: options?.tag,
withSubtasks: true
}
});
console.log('📋 Tasks fetched:', response);
return response as TaskMasterTask[];
},
staleTime: 0 // Consider data stale immediately
});
}
// Hook to fetch a single task with full details
export function useTaskDetails(taskId: string) {
const { sendMessage } = useVSCodeContext();
return useQuery({
queryKey: taskKeys.detail(taskId),
queryFn: async () => {
const response = await sendMessage({
type: 'mcpRequest',
tool: 'get_task',
params: {
id: taskId
}
});
// Parse the MCP response
let fullTaskData = null;
if (response?.data?.content?.[0]?.text) {
try {
const parsed = JSON.parse(response.data.content[0].text);
fullTaskData = parsed.data;
} catch (e) {
console.error('Failed to parse MCP response:', e);
}
} else if (response?.data?.data) {
fullTaskData = response.data.data;
}
return fullTaskData as TaskMasterTask;
},
enabled: !!taskId
});
}
// Hook to update task status
export function useUpdateTaskStatus() {
const { sendMessage } = useVSCodeContext();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
taskId,
newStatus
}: {
taskId: string;
newStatus: TaskMasterTask['status'];
}) => {
const response = await sendMessage({
type: 'updateTaskStatus',
data: { taskId, newStatus }
});
return { taskId, newStatus, response };
},
// Optimistic update to prevent snap-back
onMutate: async ({ taskId, newStatus }) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: taskKeys.all });
// Snapshot the previous value
const previousTasks = queryClient.getQueriesData({
queryKey: taskKeys.all
});
// Optimistically update all task queries
queryClient.setQueriesData({ queryKey: taskKeys.all }, (old: any) => {
if (!old) return old;
// Handle both array and object responses
if (Array.isArray(old)) {
return old.map((task: TaskMasterTask) =>
task.id === taskId ? { ...task, status: newStatus } : task
);
}
return old;
});
// Return a context object with the snapshot
return { previousTasks };
},
// If the mutation fails, roll back to the previous value
onError: (err, variables, context) => {
if (context?.previousTasks) {
context.previousTasks.forEach(([queryKey, data]) => {
queryClient.setQueryData(queryKey, data);
});
}
},
// Always refetch after error or success to ensure consistency
onSettled: () => {
queryClient.invalidateQueries({ queryKey: taskKeys.all });
}
});
}
// Hook to update task content
export function useUpdateTask() {
const { sendMessage } = useVSCodeContext();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
taskId,
updates,
options = {}
}: {
taskId: string;
updates: TaskUpdates | { description: string };
options?: { append?: boolean; research?: boolean };
}) => {
console.log('🔄 Updating task:', taskId, updates, options);
const response = await sendMessage({
type: 'updateTask',
data: { taskId, updates, options }
});
console.log('📥 Update task response:', response);
// Check for error in response
if (response && typeof response === 'object' && 'error' in response) {
throw new Error(response.error || 'Failed to update task');
}
return response;
},
onSuccess: async (data, variables) => {
console.log('✅ Task update successful, invalidating all task queries');
console.log('Response data:', data);
console.log('Task ID:', variables.taskId);
// Invalidate ALL task-related queries (same as handleRefresh)
await queryClient.invalidateQueries({
queryKey: taskKeys.all
});
console.log(
'🔄 All task queries invalidated for task:',
variables.taskId
);
}
});
}
// Hook to update subtask
export function useUpdateSubtask() {
const { sendMessage } = useVSCodeContext();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
taskId,
prompt,
options = {}
}: {
taskId: string;
prompt: string;
options?: { research?: boolean };
}) => {
console.log('🔄 Updating subtask:', taskId, prompt, options);
const response = await sendMessage({
type: 'updateSubtask',
data: { taskId, prompt, options }
});
console.log('📥 Update subtask response:', response);
// Check for error in response
if (response && typeof response === 'object' && 'error' in response) {
throw new Error(response.error || 'Failed to update subtask');
}
return response;
},
onSuccess: async (data, variables) => {
console.log(
'✅ Subtask update successful, invalidating all task queries'
);
console.log('Subtask ID:', variables.taskId);
// Invalidate ALL task-related queries (same as handleRefresh)
await queryClient.invalidateQueries({
queryKey: taskKeys.all
});
console.log(
'🔄 All task queries invalidated for subtask:',
variables.taskId
);
}
});
}
// Hook to scope up task complexity
export function useScopeUpTask() {
const { sendMessage } = useVSCodeContext();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
taskId,
strength = 'regular',
prompt,
options = {}
}: {
taskId: string;
strength?: 'light' | 'regular' | 'heavy';
prompt?: string;
options?: { research?: boolean };
}) => {
console.log('🔄 Scoping up task:', taskId, strength, prompt, options);
const response = await sendMessage({
type: 'mcpRequest',
tool: 'scope_up_task',
params: {
id: String(taskId),
strength,
prompt,
research: options.research || false
}
});
console.log('📥 Scope up task response:', response);
// Check for error in response
if (response && typeof response === 'object' && 'error' in response) {
throw new Error(response.error || 'Failed to scope up task');
}
return response;
},
onSuccess: async (data, variables) => {
console.log('✅ Task scope up successful, invalidating all task queries');
console.log('Task ID:', variables.taskId);
// Invalidate ALL task-related queries
await queryClient.invalidateQueries({
queryKey: taskKeys.all
});
console.log(
'🔄 All task queries invalidated for scoped up task:',
variables.taskId
);
}
});
}
// Hook to scope down task complexity
export function useScopeDownTask() {
const { sendMessage } = useVSCodeContext();
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
taskId,
strength = 'regular',
prompt,
options = {}
}: {
taskId: string;
strength?: 'light' | 'regular' | 'heavy';
prompt?: string;
options?: { research?: boolean };
}) => {
console.log('🔄 Scoping down task:', taskId, strength, prompt, options);
const response = await sendMessage({
type: 'mcpRequest',
tool: 'scope_down_task',
params: {
id: String(taskId),
strength,
prompt,
research: options.research || false
}
});
console.log('📥 Scope down task response:', response);
// Check for error in response
if (response && typeof response === 'object' && 'error' in response) {
throw new Error(response.error || 'Failed to scope down task');
}
return response;
},
onSuccess: async (data, variables) => {
console.log(
'✅ Task scope down successful, invalidating all task queries'
);
console.log('Task ID:', variables.taskId);
// Invalidate ALL task-related queries
await queryClient.invalidateQueries({
queryKey: taskKeys.all
});
console.log(
'🔄 All task queries invalidated for scoped down task:',
variables.taskId
);
}
});
}
```
--------------------------------------------------------------------------------
/src/profiles/base-profile.js:
--------------------------------------------------------------------------------
```javascript
// Base profile factory for rule-transformer
import path from 'path';
/**
* Creates a standardized profile configuration for different editors
* @param {Object} editorConfig - Editor-specific configuration
* @param {string} editorConfig.name - Profile name (e.g., 'cursor', 'vscode')
* @param {string} [editorConfig.displayName] - Display name for the editor (defaults to name)
* @param {string} editorConfig.url - Editor website URL
* @param {string} editorConfig.docsUrl - Editor documentation URL
* @param {string} editorConfig.profileDir - Directory for profile configuration
* @param {string} [editorConfig.rulesDir] - Directory for rules files (defaults to profileDir/rules)
* @param {boolean} [editorConfig.mcpConfig=true] - Whether to create MCP configuration
* @param {string} [editorConfig.mcpConfigName='mcp.json'] - Name of MCP config file
* @param {string} [editorConfig.fileExtension='.mdc'] - Source file extension
* @param {string} [editorConfig.targetExtension='.md'] - Target file extension
* @param {Object} [editorConfig.toolMappings={}] - Tool name mappings
* @param {Array} [editorConfig.customReplacements=[]] - Custom text replacements
* @param {Object} [editorConfig.fileMap={}] - Custom file name mappings
* @param {boolean} [editorConfig.supportsRulesSubdirectories=false] - Whether to use taskmaster/ subdirectory for taskmaster-specific rules (only Cursor uses this by default)
* @param {boolean} [editorConfig.includeDefaultRules=true] - Whether to include default rule files
* @param {Function} [editorConfig.onAdd] - Lifecycle hook for profile addition
* @param {Function} [editorConfig.onRemove] - Lifecycle hook for profile removal
* @param {Function} [editorConfig.onPostConvert] - Lifecycle hook for post-conversion
* @returns {Object} - Complete profile configuration
*/
export function createProfile(editorConfig) {
const {
name,
displayName = name,
url,
docsUrl,
profileDir = `.${name.toLowerCase()}`,
rulesDir = `${profileDir}/rules`,
mcpConfig = true,
mcpConfigName = mcpConfig ? 'mcp.json' : null,
fileExtension = '.mdc',
targetExtension = '.md',
toolMappings = {},
customReplacements = [],
fileMap = {},
supportsRulesSubdirectories = false,
includeDefaultRules = true,
onAdd,
onRemove,
onPostConvert
} = editorConfig;
const mcpConfigPath = mcpConfigName
? path.join(profileDir, mcpConfigName)
: null;
// Standard file mapping with custom overrides
// Use taskmaster subdirectory only if profile supports it
const taskmasterPrefix = supportsRulesSubdirectories ? 'taskmaster/' : '';
const defaultFileMap = {
'rules/cursor_rules.mdc': `${name.toLowerCase()}_rules${targetExtension}`,
'rules/dev_workflow.mdc': `${taskmasterPrefix}dev_workflow${targetExtension}`,
'rules/self_improve.mdc': `self_improve${targetExtension}`,
'rules/taskmaster.mdc': `${taskmasterPrefix}taskmaster${targetExtension}`
};
// Build final fileMap - merge defaults with custom entries when includeDefaultRules is true
const finalFileMap = includeDefaultRules
? { ...defaultFileMap, ...fileMap }
: fileMap;
// Base global replacements that work for all editors
const baseGlobalReplacements = [
// Handle URLs in any context
{ from: /cursor\.so/gi, to: url },
{ from: /cursor\s*\.\s*so/gi, to: url },
{ from: /https?:\/\/cursor\.so/gi, to: `https://${url}` },
{ from: /https?:\/\/www\.cursor\.so/gi, to: `https://www.${url}` },
// Handle tool references
{ from: /\bedit_file\b/gi, to: toolMappings.edit_file || 'edit_file' },
{
from: /\bsearch tool\b/gi,
to: `${toolMappings.search || 'search'} tool`
},
{ from: /\bSearch Tool\b/g, to: `${toolMappings.search || 'Search'} Tool` },
// Handle basic terms with proper case handling
{
from: /\bcursor\b/gi,
to: (match) =>
match.charAt(0) === 'C' ? displayName : name.toLowerCase()
},
{ from: /Cursor/g, to: displayName },
{ from: /CURSOR/g, to: displayName.toUpperCase() },
// Handle file extensions if different
...(targetExtension !== fileExtension
? [
{
from: new RegExp(`\\${fileExtension}(?!\\])\\b`, 'g'),
to: targetExtension
}
]
: []),
// Handle documentation URLs
{ from: /docs\.cursor\.com/gi, to: docsUrl },
// Custom editor-specific replacements
...customReplacements
];
// Standard tool mappings
const defaultToolMappings = {
search: 'search',
read_file: 'read_file',
edit_file: 'edit_file',
create_file: 'create_file',
run_command: 'run_command',
terminal_command: 'terminal_command',
use_mcp: 'use_mcp',
switch_mode: 'switch_mode',
...toolMappings
};
// Create conversion config
const conversionConfig = {
// Profile name replacements
profileTerms: [
{ from: /cursor\.so/g, to: url },
{ from: /\[cursor\.so\]/g, to: `[${url}]` },
{ from: /href="https:\/\/cursor\.so/g, to: `href="https://${url}` },
{ from: /\(https:\/\/cursor\.so/g, to: `(https://${url}` },
{
from: /\bcursor\b/gi,
to: (match) => (match === 'Cursor' ? displayName : name.toLowerCase())
},
{ from: /Cursor/g, to: displayName }
],
// File extension replacements
fileExtensions:
targetExtension !== fileExtension
? [
{
from: new RegExp(`\\${fileExtension}\\b`, 'g'),
to: targetExtension
}
]
: [],
// Documentation URL replacements
docUrls: [
{
from: new RegExp(`https:\\/\\/docs\\.cursor\\.com\\/[^\\s)'\"]+`, 'g'),
to: (match) => match.replace('docs.cursor.com', docsUrl)
},
{
from: new RegExp(`https:\\/\\/${docsUrl}\\/`, 'g'),
to: `https://${docsUrl}/`
}
],
// Tool references - direct replacements
toolNames: defaultToolMappings,
// Tool references in context - more specific replacements
toolContexts: Object.entries(defaultToolMappings).flatMap(
([original, mapped]) => [
{
from: new RegExp(`\\b${original} tool\\b`, 'g'),
to: `${mapped} tool`
},
{ from: new RegExp(`\\bthe ${original}\\b`, 'g'), to: `the ${mapped}` },
{ from: new RegExp(`\\bThe ${original}\\b`, 'g'), to: `The ${mapped}` },
{
from: new RegExp(`\\bCursor ${original}\\b`, 'g'),
to: `${displayName} ${mapped}`
}
]
),
// Tool group and category names
toolGroups: [
{ from: /\bSearch tools\b/g, to: 'Read Group tools' },
{ from: /\bEdit tools\b/g, to: 'Edit Group tools' },
{ from: /\bRun tools\b/g, to: 'Command Group tools' },
{ from: /\bMCP servers\b/g, to: 'MCP Group tools' },
{ from: /\bSearch Group\b/g, to: 'Read Group' },
{ from: /\bEdit Group\b/g, to: 'Edit Group' },
{ from: /\bRun Group\b/g, to: 'Command Group' }
],
// File references in markdown links
fileReferences: {
pathPattern: /\[(.+?)\]\(mdc:\.cursor\/rules\/(.+?)\.mdc\)/g,
replacement: (match, text, filePath) => {
const baseName = path.basename(filePath, '.mdc');
const newFileName =
finalFileMap[`rules/${baseName}.mdc`] ||
`${baseName}${targetExtension}`;
// Update the link text to match the new filename (strip directory path for display)
const newLinkText = path.basename(newFileName);
// For Cursor, keep the mdc: protocol; for others, use standard relative paths
if (name.toLowerCase() === 'cursor') {
return `[${newLinkText}](mdc:${rulesDir}/${newFileName})`;
} else {
return `[${newLinkText}](${rulesDir}/${newFileName})`;
}
}
}
};
function getTargetRuleFilename(sourceFilename) {
if (finalFileMap[sourceFilename]) {
return finalFileMap[sourceFilename];
}
return targetExtension !== fileExtension
? sourceFilename.replace(
new RegExp(`\\${fileExtension}$`),
targetExtension
)
: sourceFilename;
}
return {
profileName: name, // Use name for programmatic access (tests expect this)
displayName: displayName, // Keep displayName for UI purposes
profileDir,
rulesDir,
mcpConfig,
mcpConfigName,
mcpConfigPath,
supportsRulesSubdirectories,
includeDefaultRules,
fileMap: finalFileMap,
globalReplacements: baseGlobalReplacements,
conversionConfig,
getTargetRuleFilename,
targetExtension,
// Optional lifecycle hooks
...(onAdd && { onAddRulesProfile: onAdd }),
...(onRemove && { onRemoveRulesProfile: onRemove }),
...(onPostConvert && { onPostConvertRulesProfile: onPostConvert })
};
}
// Common tool mappings for editors that share similar tool sets
export const COMMON_TOOL_MAPPINGS = {
// Most editors (Cursor, Cline, Windsurf) keep original tool names
STANDARD: {},
// Roo Code uses different tool names
ROO_STYLE: {
edit_file: 'apply_diff',
search: 'search_files',
create_file: 'write_to_file',
run_command: 'execute_command',
terminal_command: 'execute_command',
use_mcp: 'use_mcp_tool'
}
};
```