This is page 20 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
--------------------------------------------------------------------------------
/src/profiles/amp.js:
--------------------------------------------------------------------------------
```javascript
// Amp 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';
/**
* Transform standard MCP config format to Amp format
* @param {Object} mcpConfig - Standard MCP configuration object
* @returns {Object} - Transformed Amp configuration object
*/
function transformToAmpFormat(mcpConfig) {
const ampConfig = {};
// Transform mcpServers to amp.mcpServers
if (mcpConfig.mcpServers) {
ampConfig['amp.mcpServers'] = mcpConfig.mcpServers;
}
// Preserve any other existing settings
for (const [key, value] of Object.entries(mcpConfig)) {
if (key !== 'mcpServers') {
ampConfig[key] = value;
}
}
return ampConfig;
}
// Lifecycle functions for Amp profile
function onAddRulesProfile(targetDir, assetsDir) {
// Handle AGENT.md import for non-destructive integration (Amp uses AGENT.md, copies from AGENTS.md)
const sourceFile = path.join(assetsDir, 'AGENTS.md');
const userAgentFile = path.join(targetDir, 'AGENT.md');
const taskMasterAgentFile = path.join(targetDir, '.taskmaster', 'AGENT.md');
const importLine = '@./.taskmaster/AGENT.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 AGENT.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/AGENT.md
fs.copyFileSync(sourceFile, taskMasterAgentFile);
log(
'debug',
`[Amp] Created Task Master instructions at ${taskMasterAgentFile}`
);
// Handle user's AGENT.md
if (fs.existsSync(userAgentFile)) {
// Check if import already exists
const content = fs.readFileSync(userAgentFile, 'utf8');
if (!content.includes(importLine)) {
// Append import section at the end
const updatedContent = content.trim() + '\n' + importSection + '\n';
fs.writeFileSync(userAgentFile, updatedContent);
log(
'info',
`[Amp] Added Task Master import to existing ${userAgentFile}`
);
} else {
log(
'info',
`[Amp] Task Master import already present in ${userAgentFile}`
);
}
} else {
// Create minimal AGENT.md with the import section
const minimalContent = `# Amp Instructions\n${importSection}\n`;
fs.writeFileSync(userAgentFile, minimalContent);
log('info', `[Amp] Created ${userAgentFile} with Task Master import`);
}
} catch (err) {
log('error', `[Amp] Failed to set up Amp instructions: ${err.message}`);
}
}
// MCP transformation will be handled in onPostConvertRulesProfile
}
function onRemoveRulesProfile(targetDir) {
// Clean up AGENT.md import (Amp uses AGENT.md, not AGENTS.md)
const userAgentFile = path.join(targetDir, 'AGENT.md');
const taskMasterAgentFile = path.join(targetDir, '.taskmaster', 'AGENT.md');
const importLine = '@./.taskmaster/AGENT.md';
try {
// Remove Task Master AGENT.md from .taskmaster
if (fs.existsSync(taskMasterAgentFile)) {
fs.rmSync(taskMasterAgentFile, { force: true });
log('debug', `[Amp] Removed ${taskMasterAgentFile}`);
}
// Clean up import from user's AGENT.md
if (fs.existsSync(userAgentFile)) {
const content = fs.readFileSync(userAgentFile, '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 === '# Amp Instructions' || updatedContent === '') {
// File only contained our import, remove it
fs.rmSync(userAgentFile, { force: true });
log('debug', `[Amp] Removed empty ${userAgentFile}`);
} else {
// Write back without the import
fs.writeFileSync(userAgentFile, updatedContent + '\n');
log('debug', `[Amp] Removed Task Master import from ${userAgentFile}`);
}
}
} catch (err) {
log('error', `[Amp] Failed to remove Amp instructions: ${err.message}`);
}
// MCP Removal: Remove amp.mcpServers section
const mcpConfigPath = path.join(targetDir, '.vscode', 'settings.json');
if (!fs.existsSync(mcpConfigPath)) {
log('debug', '[Amp] No .vscode/settings.json found to clean up');
return;
}
try {
// Read the current config
const configContent = fs.readFileSync(mcpConfigPath, 'utf8');
const config = JSON.parse(configContent);
// Check if it has the amp.mcpServers section and task-master-ai server
if (
config['amp.mcpServers'] &&
config['amp.mcpServers']['task-master-ai']
) {
// Remove task-master-ai server
delete config['amp.mcpServers']['task-master-ai'];
// Check if there are other MCP servers in amp.mcpServers
const remainingServers = Object.keys(config['amp.mcpServers']);
if (remainingServers.length === 0) {
// No other servers, remove entire amp.mcpServers section
delete config['amp.mcpServers'];
log('debug', '[Amp] Removed empty amp.mcpServers section');
}
// Check if config is now empty
const remainingKeys = Object.keys(config);
if (remainingKeys.length === 0) {
// Config is empty, remove entire file
fs.rmSync(mcpConfigPath, { force: true });
log('info', '[Amp] Removed empty settings.json file');
// Check if .vscode directory is empty
const vscodeDirPath = path.join(targetDir, '.vscode');
if (fs.existsSync(vscodeDirPath)) {
const remainingContents = fs.readdirSync(vscodeDirPath);
if (remainingContents.length === 0) {
fs.rmSync(vscodeDirPath, { recursive: true, force: true });
log('debug', '[Amp] Removed empty .vscode directory');
}
}
} else {
// Write back the modified config
fs.writeFileSync(
mcpConfigPath,
JSON.stringify(config, null, '\t') + '\n'
);
log(
'info',
'[Amp] Removed TaskMaster from settings.json, preserved other configurations'
);
}
} else {
log('debug', '[Amp] TaskMaster not found in amp.mcpServers');
}
} catch (error) {
log('error', `[Amp] Failed to clean up settings.json: ${error.message}`);
}
}
function onPostConvertRulesProfile(targetDir, assetsDir) {
// Handle AGENT.md setup (same as onAddRulesProfile)
onAddRulesProfile(targetDir, assetsDir);
// Transform MCP config to Amp format
const mcpConfigPath = path.join(targetDir, '.vscode', 'settings.json');
if (!fs.existsSync(mcpConfigPath)) {
log('debug', '[Amp] No .vscode/settings.json found to transform');
return;
}
try {
// Read the generated standard MCP config
const mcpConfigContent = fs.readFileSync(mcpConfigPath, 'utf8');
const mcpConfig = JSON.parse(mcpConfigContent);
// Check if it's already in Amp format (has amp.mcpServers)
if (mcpConfig['amp.mcpServers']) {
log(
'info',
'[Amp] settings.json already in Amp format, skipping transformation'
);
return;
}
// Transform to Amp format
const ampConfig = transformToAmpFormat(mcpConfig);
// Write back the transformed config with proper formatting
fs.writeFileSync(
mcpConfigPath,
JSON.stringify(ampConfig, null, '\t') + '\n'
);
log('info', '[Amp] Transformed settings.json to Amp format');
log('debug', '[Amp] Renamed mcpServers to amp.mcpServers');
} catch (error) {
log('error', `[Amp] Failed to transform settings.json: ${error.message}`);
}
}
// Create and export amp profile using the base factory
export const ampProfile = createProfile({
name: 'amp',
displayName: 'Amp',
url: 'ampcode.com',
docsUrl: 'ampcode.com/manual',
profileDir: '.vscode',
rulesDir: '.',
mcpConfig: true,
mcpConfigName: 'settings.json',
includeDefaultRules: false,
fileMap: {
'AGENTS.md': '.taskmaster/AGENT.md'
},
onAdd: onAddRulesProfile,
onRemove: onRemoveRulesProfile,
onPostConvert: onPostConvertRulesProfile
});
// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };
```
--------------------------------------------------------------------------------
/apps/docs/capabilities/rpg-method.mdx:
--------------------------------------------------------------------------------
```markdown
---
title: RPG Method for PRD Creation
sidebarTitle: "RPG Method"
---
# Repository Planning Graph (RPG) Method
The RPG (Repository Planning Graph) method is an advanced approach to creating Product Requirements Documents that generate highly-structured, dependency-aware task graphs. It's based on Microsoft Research's methodology for scalable codebase generation.
## When to Use RPG
Use the RPG template (`example_prd_rpg.md`) for:
- **Complex multi-module systems** with intricate dependencies
- **Large-scale codebases** being built from scratch
- **Projects requiring explicit architecture** and clear module boundaries
- **Teams needing dependency visibility** for parallel development
For simpler features or smaller projects, the standard `example_prd.md` template may be more appropriate.
---
## Core Principles
### 1. Dual-Semantics
Separate **functional** thinking (WHAT) from **structural** thinking (HOW):
```
Functional: "Data Validation capability with schema checking and rule enforcement"
↓
Structural: "src/validation/ with schema-validator.js and rule-validator.js"
```
This separation prevents mixing concerns and creates clearer module boundaries.
### 2. Explicit Dependencies
Never assume dependencies - always state them explicitly:
```
Good:
Module: data-ingestion
Depends on: [schema-validator, config-manager]
Bad:
Module: data-ingestion
(Assumes schema-validator exists somewhere)
```
Explicit dependencies enable:
- Topological ordering of implementation
- Parallel development of independent modules
- Clear build/test order
- Early detection of circular dependencies
### 3. Topological Order
Build foundation layers before higher layers:
```
Phase 0 (Foundation): error-handling, base-types, config
↓
Phase 1 (Data): validation, ingestion (depend on Phase 0)
↓
Phase 2 (Core): algorithms, pipelines (depend on Phase 1)
↓
Phase 3 (API): routes, handlers (depend on Phase 2)
```
Task Master automatically orders tasks based on this dependency chain.
### 4. Progressive Refinement
Start broad, refine iteratively:
1. High-level capabilities → Main tasks
2. Features per capability → Subtasks
3. Implementation details → Expanded subtasks
---
## Template Structure
The RPG template guides you through 7 key sections:
### 1. Overview
- Problem statement
- Target users
- Success metrics
### 2. Functional Decomposition (WHAT)
- High-level capability domains
- Features per capability
- Inputs/outputs/behavior for each feature
**Example:**
```
Capability: Data Management
Feature: Schema validation
Description: Validate JSON against defined schemas
Inputs: JSON object, schema definition
Outputs: Validation result + error details
Behavior: Iterate fields, check types, enforce constraints
```
### 3. Structural Decomposition (HOW)
- Repository folder structure
- Module-to-capability mapping
- File organization
- Public interfaces/exports
**Example:**
```
Capability: Data Management
→ Maps to: src/data/
├── schema-validator.js (Schema validation feature)
├── rule-validator.js (Rule validation feature)
└── index.js (Exports)
```
### 4. Dependency Graph (CRITICAL)
- Foundation layer (no dependencies)
- Each subsequent layer's dependencies
- Explicit "depends on" declarations
**Example:**
```
Foundation Layer (Phase 0):
- error-handling: No dependencies
- base-types: No dependencies
Data Layer (Phase 1):
- schema-validator: Depends on [base-types, error-handling]
- data-ingestion: Depends on [schema-validator]
```
### 5. Implementation Roadmap
- Phases with entry/exit criteria
- Tasks grouped by phase
- Clear deliverables per phase
### 6. Test Strategy
- Test pyramid ratios
- Coverage requirements
- Critical test scenarios per module
- Guidelines for test generation
### 7. Architecture & Risks
- Technical architecture
- Data models
- Technology decisions
- Risk mitigation strategies
---
## Using RPG with Task Master
### Step 1: Create PRD with RPG Template
Use a code-context-aware tool to fill out the template:
```bash
# In Claude Code, Cursor, or similar
"Create a PRD using @.taskmaster/templates/example_prd_rpg.txt for [your project]"
```
**Why code context matters:** The AI needs to understand your existing codebase to make informed decisions about:
- Module boundaries
- Dependency relationships
- Integration points
- Naming conventions
**Recommended tools:**
- Claude Code (claude-code CLI)
- Cursor/Windsurf
- Gemini CLI (large contexts)
- Codex/Grok CLI
### Step 2: Parse PRD into Tasks
```bash
task-master parse-prd .taskmaster/docs/your-prd.md --research
```
Task Master will:
1. Extract capabilities → Main tasks
2. Extract features → Subtasks
3. Parse dependencies → Task dependencies
4. Order by phases → Task priorities
**Result:** A dependency-aware task graph ready for topological execution.
### Step 3: Analyze Complexity
```bash
task-master analyze-complexity --research
```
Review the complexity report to identify tasks that need expansion.
### Step 4: Expand Tasks
```bash
task-master expand --all --research
```
Break down complex tasks into manageable subtasks while preserving dependency chains.
---
## RPG Benefits
### For Solo Developers
- Clear roadmap for implementing complex features
- Prevents architectural mistakes early
- Explicit dependency tracking avoids integration issues
- Enables resuming work after interruptions
### For Teams
- Parallel development of independent modules
- Clear contracts between modules (explicit dependencies)
- Reduced merge conflicts (proper module boundaries)
- Onboarding aid (architectural overview in PRD)
### For AI Agents
- Structured context for code generation
- Clear scope boundaries per task
- Dependency awareness prevents incomplete implementations
- Test strategy guidance for TDD workflows
---
## RPG vs Standard Template
| Aspect | Standard Template | RPG Template |
|--------|------------------|--------------|
| **Best for** | Simple features | Complex systems |
| **Dependency handling** | Implicit | Explicit graph |
| **Structure guidance** | Minimal | Step-by-step |
| **Examples** | Few | Inline good/bad examples |
| **Module boundaries** | Vague | Precise mapping |
| **Task ordering** | Manual | Automatic (topological) |
| **Learning curve** | Low | Medium |
| **Resulting task quality** | Good | Excellent |
---
## Tips for Best Results
### 1. Spend Time on Dependencies
The dependency graph section is the most valuable. List all dependencies explicitly, even if they seem obvious.
### 2. Keep Features Atomic
Each feature should be independently testable. If a feature description is vague ("handle data"), break it into specific features.
### 3. Progressive Refinement
Don't try to get everything perfect on the first pass:
1. Fill out high-level sections
2. Review and refine
3. Add detail where needed
4. Let `task-master expand` break down complex tasks further
### 4. Use Research Mode
```bash
task-master parse-prd --research
```
The `--research` flag leverages AI to enhance task generation with domain knowledge.
### 5. Validate Early
```bash
task-master validate-dependencies
```
Check for circular dependencies or orphaned modules before starting implementation.
---
## Common Pitfalls
### ❌ Mixing Functional and Structural
```
Bad: "Capability: validation.js"
Good: "Capability: Data Validation" → maps to "src/validation/"
```
### ❌ Vague Module Boundaries
```
Bad: "Module: utils"
Good: "Module: string-utilities" with clear exports
```
### ❌ Implicit Dependencies
```
Bad: "Module: API handlers (needs validation)"
Good: "Module: API handlers, Depends on: [validation, error-handling]"
```
### ❌ Skipping Test Strategy
Without test strategy, the AI won't know what to test during implementation.
---
## Example Workflow
1. **Discuss idea with AI**: Explain your project concept
2. **Reference RPG template**: Show AI the `example_prd_rpg.md`
3. **Co-create PRD**: Work through each section with AI guidance
4. **Save to docs**: Place in `.taskmaster/docs/your-project.md` (use `.md` for better editor support)
5. **Parse PRD**: `task-master parse-prd .taskmaster/docs/your-project.md --research`
6. **Analyze**: `task-master analyze-complexity --research`
7. **Expand**: `task-master expand --all --research`
8. **Start work**: `task-master next`
---
## Further Reading
- [PRD Creation and Parsing Guide](/getting-started/quick-start/prd-quick)
- [Task Structure Documentation](/capabilities/task-structure)
- [Microsoft Research RPG Paper](https://arxiv.org/abs/2410.21376) (Original methodology)
---
<Tip>
The RPG template includes inline `<instruction>` and `<example>` blocks that teach the method as you use it. Read these sections carefully - they provide valuable guidance at each decision point.
</Tip>
```
--------------------------------------------------------------------------------
/apps/extension/src/utils/connectionManager.ts:
--------------------------------------------------------------------------------
```typescript
import * as vscode from 'vscode';
import { logger } from './logger';
import {
MCPClientManager,
type MCPConfig,
type MCPServerStatus
} from './mcpClient';
export interface ConnectionEvent {
type: 'connected' | 'disconnected' | 'error' | 'reconnecting';
timestamp: Date;
data?: any;
}
export interface ConnectionHealth {
isHealthy: boolean;
lastSuccessfulCall?: Date;
consecutiveFailures: number;
averageResponseTime: number;
uptime: number;
}
export class ConnectionManager {
private mcpClient: MCPClientManager | null = null;
private config: MCPConfig;
private connectionEvents: ConnectionEvent[] = [];
private health: ConnectionHealth = {
isHealthy: false,
consecutiveFailures: 0,
averageResponseTime: 0,
uptime: 0
};
private startTime: Date | null = null;
private healthCheckInterval: NodeJS.Timeout | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectBackoffMs = 1000; // Start with 1 second
private maxBackoffMs = 30000; // Max 30 seconds
private isReconnecting = false;
// Event handlers
private onConnectionChange?: (
status: MCPServerStatus,
health: ConnectionHealth
) => void;
private onConnectionEvent?: (event: ConnectionEvent) => void;
constructor(config: MCPConfig) {
this.config = config;
this.mcpClient = new MCPClientManager(config);
}
/**
* Set event handlers
*/
setEventHandlers(handlers: {
onConnectionChange?: (
status: MCPServerStatus,
health: ConnectionHealth
) => void;
onConnectionEvent?: (event: ConnectionEvent) => void;
}) {
this.onConnectionChange = handlers.onConnectionChange;
this.onConnectionEvent = handlers.onConnectionEvent;
}
/**
* Connect with automatic retry and health monitoring
*/
async connect(): Promise<void> {
try {
if (!this.mcpClient) {
throw new Error('MCP client not initialized');
}
this.logEvent({ type: 'reconnecting', timestamp: new Date() });
await this.mcpClient.connect();
this.reconnectAttempts = 0;
this.reconnectBackoffMs = 1000;
this.isReconnecting = false;
this.startTime = new Date();
this.updateHealth();
this.startHealthMonitoring();
this.logEvent({ type: 'connected', timestamp: new Date() });
logger.log('Connection manager: Successfully connected');
} catch (error) {
this.logEvent({
type: 'error',
timestamp: new Date(),
data: {
error: error instanceof Error ? error.message : 'Unknown error'
}
});
await this.handleConnectionFailure(error);
throw error;
}
}
/**
* Disconnect and stop health monitoring
*/
async disconnect(): Promise<void> {
this.stopHealthMonitoring();
this.isReconnecting = false;
if (this.mcpClient) {
await this.mcpClient.disconnect();
}
this.health.isHealthy = false;
this.startTime = null;
this.logEvent({ type: 'disconnected', timestamp: new Date() });
this.notifyConnectionChange();
}
/**
* Get current connection status
*/
getStatus(): MCPServerStatus {
return this.mcpClient?.getStatus() || { isRunning: false };
}
/**
* Get connection health metrics
*/
getHealth(): ConnectionHealth {
this.updateHealth();
return { ...this.health };
}
/**
* Get recent connection events
*/
getEvents(limit = 10): ConnectionEvent[] {
return this.connectionEvents.slice(-limit);
}
/**
* Test connection with performance monitoring
*/
async testConnection(): Promise<{
success: boolean;
responseTime: number;
error?: string;
}> {
if (!this.mcpClient) {
return {
success: false,
responseTime: 0,
error: 'Client not initialized'
};
}
const startTime = Date.now();
try {
const success = await this.mcpClient.testConnection();
const responseTime = Date.now() - startTime;
if (success) {
this.health.lastSuccessfulCall = new Date();
this.health.consecutiveFailures = 0;
this.updateAverageResponseTime(responseTime);
} else {
this.health.consecutiveFailures++;
}
this.updateHealth();
this.notifyConnectionChange();
return { success, responseTime };
} catch (error) {
const responseTime = Date.now() - startTime;
this.health.consecutiveFailures++;
this.updateHealth();
this.notifyConnectionChange();
return {
success: false,
responseTime,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}
/**
* Call MCP tool with automatic retry and health monitoring
*/
async callTool(
toolName: string,
arguments_: Record<string, unknown>
): Promise<any> {
if (!this.mcpClient) {
throw new Error('MCP client not initialized');
}
const startTime = Date.now();
try {
const result = await this.mcpClient.callTool(toolName, arguments_);
const responseTime = Date.now() - startTime;
this.health.lastSuccessfulCall = new Date();
this.health.consecutiveFailures = 0;
this.updateAverageResponseTime(responseTime);
this.updateHealth();
this.notifyConnectionChange();
return result;
} catch (error) {
this.health.consecutiveFailures++;
this.updateHealth();
// Attempt reconnection if connection seems lost
if (this.health.consecutiveFailures >= 3 && !this.isReconnecting) {
logger.log(
'Multiple consecutive failures detected, attempting reconnection...'
);
this.reconnectWithBackoff().catch((err) => {
logger.error('Reconnection failed:', err);
});
}
this.notifyConnectionChange();
throw error;
}
}
/**
* Update configuration and reconnect
*/
async updateConfig(newConfig: MCPConfig): Promise<void> {
this.config = newConfig;
await this.disconnect();
this.mcpClient = new MCPClientManager(newConfig);
// Attempt to reconnect with new config
try {
await this.connect();
} catch (error) {
logger.error('Failed to connect with new configuration:', error);
}
}
/**
* Start health monitoring
*/
private startHealthMonitoring(): void {
this.stopHealthMonitoring();
this.healthCheckInterval = setInterval(async () => {
try {
await this.testConnection();
} catch (error) {
logger.error('Health check failed:', error);
}
}, 15000); // Check every 15 seconds
}
/**
* Stop health monitoring
*/
private stopHealthMonitoring(): void {
if (this.healthCheckInterval) {
clearInterval(this.healthCheckInterval);
this.healthCheckInterval = null;
}
}
/**
* Handle connection failure with exponential backoff
*/
private async handleConnectionFailure(error: any): Promise<void> {
this.health.consecutiveFailures++;
this.updateHealth();
this.notifyConnectionChange();
if (
this.reconnectAttempts < this.maxReconnectAttempts &&
!this.isReconnecting
) {
await this.reconnectWithBackoff();
}
}
/**
* Reconnect with exponential backoff
*/
private async reconnectWithBackoff(): Promise<void> {
if (this.isReconnecting) {
return;
}
this.isReconnecting = true;
this.reconnectAttempts++;
const backoffMs = Math.min(
this.reconnectBackoffMs * 2 ** (this.reconnectAttempts - 1),
this.maxBackoffMs
);
logger.log(
`Attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${backoffMs}ms...`
);
await new Promise((resolve) => setTimeout(resolve, backoffMs));
try {
await this.connect();
} catch (error) {
logger.error(
`Reconnection attempt ${this.reconnectAttempts} failed:`,
error
);
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
this.isReconnecting = false;
vscode.window.showErrorMessage(
`Failed to reconnect to Task Master after ${this.maxReconnectAttempts} attempts. Please check your configuration and try manually reconnecting.`
);
} else {
// Try again
await this.reconnectWithBackoff();
}
}
}
/**
* Update health metrics
*/
private updateHealth(): void {
const status = this.getStatus();
this.health.isHealthy =
status.isRunning && this.health.consecutiveFailures < 3;
if (this.startTime) {
this.health.uptime = Date.now() - this.startTime.getTime();
}
}
/**
* Update average response time
*/
private updateAverageResponseTime(responseTime: number): void {
// Simple moving average calculation
if (this.health.averageResponseTime === 0) {
this.health.averageResponseTime = responseTime;
} else {
this.health.averageResponseTime =
this.health.averageResponseTime * 0.8 + responseTime * 0.2;
}
}
/**
* Log connection event
*/
private logEvent(event: ConnectionEvent): void {
this.connectionEvents.push(event);
// Keep only last 100 events
if (this.connectionEvents.length > 100) {
this.connectionEvents = this.connectionEvents.slice(-100);
}
if (this.onConnectionEvent) {
this.onConnectionEvent(event);
}
}
/**
* Notify connection change
*/
private notifyConnectionChange(): void {
if (this.onConnectionChange) {
this.onConnectionChange(this.getStatus(), this.getHealth());
}
}
}
```
--------------------------------------------------------------------------------
/mcp-server/src/core/utils/path-utils.js:
--------------------------------------------------------------------------------
```javascript
import path from 'path';
import {
findTasksPath as coreFindTasksPath,
findPRDPath as coreFindPrdPath,
findComplexityReportPath as coreFindComplexityReportPath,
resolveComplexityReportOutputPath as coreResolveComplexityReportOutputPath,
findProjectRoot as coreFindProjectRoot,
normalizeProjectRoot
} from '../../../../src/utils/path-utils.js';
import { PROJECT_MARKERS } from '../../../../src/constants/paths.js';
/**
* MCP-specific path utilities that extend core path utilities with session support
* This module handles session-specific path resolution for the MCP server
*/
/**
* Silent logger for MCP context to prevent console output
*/
const silentLogger = {
info: () => {},
warn: () => {},
error: () => {},
debug: () => {},
success: () => {}
};
/**
* Cache for last found project root to improve performance
*/
export const lastFoundProjectRoot = null;
/**
* Find PRD file with MCP support
* @param {string} [explicitPath] - Explicit path to PRD file (highest priority)
* @param {Object} [args] - Arguments object for context
* @param {Object} [log] - Logger object to prevent console logging
* @returns {string|null} - Resolved path to PRD file or null if not found
*/
export function findPrdPath(explicitPath, args = null, log = silentLogger) {
return coreFindPrdPath(explicitPath, args, log);
}
/**
* Resolve tasks.json path from arguments
* Prioritizes explicit path parameter, then uses fallback logic
* @param {Object} args - Arguments object containing projectRoot and optional file path
* @param {Object} [log] - Logger object to prevent console logging
* @returns {string|null} - Resolved path to tasks.json or null if not found
*/
export function resolveTasksPath(args, log = silentLogger) {
// Get explicit path from args.file if provided
const explicitPath = args?.file;
const rawProjectRoot = args?.projectRoot;
// If explicit path is provided and absolute, use it directly
if (explicitPath && path.isAbsolute(explicitPath)) {
return explicitPath;
}
// Normalize project root if provided
const projectRoot = rawProjectRoot
? normalizeProjectRoot(rawProjectRoot)
: null;
// If explicit path is relative, resolve it relative to normalized projectRoot
if (explicitPath && projectRoot) {
return path.resolve(projectRoot, explicitPath);
}
// Use core findTasksPath with explicit path and normalized projectRoot context
if (projectRoot) {
const foundPath = coreFindTasksPath(explicitPath, { projectRoot }, log);
// If core function returns null and no explicit path was provided,
// construct the expected default path as documented
if (foundPath === null && !explicitPath) {
const defaultPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
log?.info?.(
`Core findTasksPath returned null, using default path: ${defaultPath}`
);
return defaultPath;
}
return foundPath;
}
// Fallback to core function without projectRoot context
const foundPath = coreFindTasksPath(explicitPath, null, log);
// Note: When no projectRoot is available, we can't construct a default path
// so we return null and let the calling code handle the error
return foundPath;
}
/**
* Resolve PRD path from arguments
* @param {Object} args - Arguments object containing projectRoot and optional input path
* @param {Object} [log] - Logger object to prevent console logging
* @returns {string|null} - Resolved path to PRD file or null if not found
*/
export function resolvePrdPath(args, log = silentLogger) {
// Get explicit path from args.input if provided
const explicitPath = args?.input;
const rawProjectRoot = args?.projectRoot;
// If explicit path is provided and absolute, use it directly
if (explicitPath && path.isAbsolute(explicitPath)) {
return explicitPath;
}
// Normalize project root if provided
const projectRoot = rawProjectRoot
? normalizeProjectRoot(rawProjectRoot)
: null;
// If explicit path is relative, resolve it relative to normalized projectRoot
if (explicitPath && projectRoot) {
return path.resolve(projectRoot, explicitPath);
}
// Use core findPRDPath with explicit path and normalized projectRoot context
if (projectRoot) {
return coreFindPrdPath(explicitPath, { projectRoot }, log);
}
// Fallback to core function without projectRoot context
return coreFindPrdPath(explicitPath, null, log);
}
/**
* Resolve complexity report path from arguments
* @param {Object} args - Arguments object containing projectRoot and optional complexityReport path
* @param {Object} [log] - Logger object to prevent console logging
* @returns {string|null} - Resolved path to complexity report or null if not found
*/
export function resolveComplexityReportPath(args, log = silentLogger) {
// Get explicit path from args.complexityReport if provided
const explicitPath = args?.complexityReport;
const rawProjectRoot = args?.projectRoot;
const tag = args?.tag;
// If explicit path is provided and absolute, use it directly
if (explicitPath && path.isAbsolute(explicitPath)) {
return explicitPath;
}
// Normalize project root if provided
const projectRoot = rawProjectRoot
? normalizeProjectRoot(rawProjectRoot)
: null;
// If explicit path is relative, resolve it relative to normalized projectRoot
if (explicitPath && projectRoot) {
return path.resolve(projectRoot, explicitPath);
}
// Use core findComplexityReportPath with explicit path and normalized projectRoot context
if (projectRoot) {
return coreFindComplexityReportPath(
explicitPath,
{ projectRoot, tag },
log
);
}
// Fallback to core function without projectRoot context
return coreFindComplexityReportPath(explicitPath, null, log);
}
/**
* Resolve any project-relative path from arguments
* @param {string} relativePath - Relative path to resolve
* @param {Object} args - Arguments object containing projectRoot
* @returns {string} - Resolved absolute path
*/
export function resolveProjectPath(relativePath, args) {
// Ensure we have a projectRoot from args
if (!args?.projectRoot) {
throw new Error('projectRoot is required in args to resolve project paths');
}
// Normalize the project root to prevent double .taskmaster paths
const projectRoot = normalizeProjectRoot(args.projectRoot);
// If already absolute, return as-is
if (path.isAbsolute(relativePath)) {
return relativePath;
}
// Resolve relative to normalized projectRoot
return path.resolve(projectRoot, relativePath);
}
/**
* Find project root using core utility
* @param {string} [startDir] - Directory to start searching from
* @returns {string|null} - Project root path or null if not found
*/
export function findProjectRoot(startDir) {
return coreFindProjectRoot(startDir);
}
// MAIN EXPORTS FOR MCP TOOLS - these are the functions MCP tools should use
/**
* Find tasks.json path from arguments - primary MCP function
* @param {Object} args - Arguments object containing projectRoot and optional file path
* @param {Object} [log] - Log function to prevent console logging
* @returns {string|null} - Resolved path to tasks.json or null if not found
*/
export function findTasksPath(args, log = silentLogger) {
return resolveTasksPath(args, log);
}
/**
* Find complexity report path from arguments - primary MCP function
* @param {Object} args - Arguments object containing projectRoot and optional complexityReport path
* @param {Object} [log] - Log function to prevent console logging
* @returns {string|null} - Resolved path to complexity report or null if not found
*/
export function findComplexityReportPath(args, log = silentLogger) {
return resolveComplexityReportPath(args, log);
}
/**
* Resolve complexity report output path (create if needed) - primary MCP function
* @param {string|null} [explicitPath] - Explicit path to complexity report
* @param {Object} args - Arguments object containing projectRoot and tag
* @param {Object} [log] - Log function to prevent console logging
* @returns {string} - Resolved output path for complexity report
*/
export function resolveComplexityReportOutputPath(
explicitPath,
args,
log = silentLogger
) {
return coreResolveComplexityReportOutputPath(explicitPath, args, log);
}
/**
* Find PRD path - primary MCP function
* @param {string} [explicitPath] - Explicit path to PRD file
* @param {Object} [args] - Arguments object for context (not used in current implementation)
* @param {Object} [log] - Logger object to prevent console logging
* @returns {string|null} - Resolved path to PRD file or null if not found
*/
export function findPRDPath(explicitPath, args = null, log = silentLogger) {
return findPrdPath(explicitPath, args, log);
}
// Legacy aliases for backward compatibility - DEPRECATED
export const findTasksJsonPath = findTasksPath;
export const findComplexityReportJsonPath = findComplexityReportPath;
// Re-export PROJECT_MARKERS for MCP tools that import it from this module
export { PROJECT_MARKERS };
```
--------------------------------------------------------------------------------
/docs/contributor-docs/worktree-setup.md:
--------------------------------------------------------------------------------
```markdown
# Git Worktree Setup for Parallel Development
Simple git worktree setup for running multiple AI coding assistants in parallel.
## Why Worktrees?
Instead of Docker complexity, use git worktrees to create isolated working directories:
✅ **Editor Agnostic** - Works with Cursor, Windsurf, VS Code, Claude Code, etc.
✅ **Simple** - No Docker, no containers, just git
✅ **Fast** - Instant setup, shared git history
✅ **Flexible** - Each worktree can be on a different branch
✅ **Task Master Works** - Full access to `.taskmaster/` in each worktree
## Quick Start
### 1. Create a Worktree
```bash
# Using current branch as base
./scripts/create-worktree.sh
# Or specify a branch name
./scripts/create-worktree.sh feature/my-feature
```
This creates a worktree in `../claude-task-master-worktrees/<branch-name>/`
### 2. Open in Your Editor
```bash
# Navigate to the worktree
cd ../claude-task-master-worktrees/auto-main/ # (or whatever branch)
# Open with your preferred AI editor
cursor . # Cursor
code . # VS Code
windsurf . # Windsurf
claude # Claude Code CLI
```
### 3. Work in Parallel
**Main directory** (where you are now):
```bash
# Keep working normally
git checkout main
cursor .
```
**Worktree directory**:
```bash
cd ../claude-task-master-worktrees/auto-main/
# Different files, different branch, same git repo
claude
```
## Usage Examples
### Example 1: Let Claude Work Autonomously
```bash
# Create worktree
./scripts/create-worktree.sh auto/taskmaster-work
# Navigate there
cd ../claude-task-master-worktrees/auto-taskmaster-work/
# Start Claude
claude
# In Claude session
> Use task-master to get the next task and complete it
```
**Meanwhile in your main directory:**
```bash
# You keep working normally
cursor .
# No conflicts!
```
### Example 2: Multiple AI Assistants in Parallel
```bash
# Create multiple worktrees
./scripts/create-worktree.sh cursor/feature-a
./scripts/create-worktree.sh claude/feature-b
./scripts/create-worktree.sh windsurf/feature-c
# Terminal 1
cd ../claude-task-master-worktrees/cursor-feature-a/
cursor .
# Terminal 2
cd ../claude-task-master-worktrees/claude-feature-b/
claude
# Terminal 3
cd ../claude-task-master-worktrees/windsurf-feature-c/
windsurf .
```
### Example 3: Test vs Implementation
```bash
# Main directory: Write implementation
cursor .
# Worktree: Have Claude write tests
cd ../claude-task-master-worktrees/auto-main/
claude -p "Write tests for the recent changes in the main branch"
```
## How It Works
### Directory Structure
```
/Volumes/Workspace/workspace/contrib/task-master/
├── claude-task-master/ # Main directory (this one)
│ ├── .git/ # Shared git repo
│ ├── .taskmaster/ # Synced via git
│ └── your code...
│
└── claude-task-master-worktrees/ # Worktrees directory
├── auto-main/ # Worktree 1
│ ├── .git -> (points to main .git)
│ ├── .taskmaster/ # Same tasks, synced
│ └── your code... (on branch auto/main)
│
└── feature-x/ # Worktree 2
├── .git -> (points to main .git)
├── .taskmaster/
└── your code... (on branch feature/x)
```
### Shared Git Repository
All worktrees share the same `.git`:
- Commits in one worktree are immediately visible in others
- Branches are shared
- Git history is shared
- Only the working files differ
## Task Master in Worktrees
Task Master works perfectly in worktrees:
```bash
# In any worktree
task-master list # Same tasks
task-master next # Same task queue
task-master show 1.2 # Same task data
# Changes are shared (if committed/pushed)
```
### Recommended Workflow
Use **tags** to separate task contexts:
```bash
# Main directory - use default tag
task-master list
# Worktree 1 - use separate tag
cd ../claude-task-master-worktrees/auto-main/
task-master add-tag --name=claude-auto
task-master use-tag --name=claude-auto
task-master list # Shows claude-auto tasks only
```
## Managing Worktrees
### List All Worktrees
```bash
./scripts/list-worktrees.sh
# Or directly with git
git worktree list
```
### Remove a Worktree
```bash
# Remove specific worktree
git worktree remove ../claude-task-master-worktrees/auto-main/
# Or if there are uncommitted changes, force it
git worktree remove --force ../claude-task-master-worktrees/auto-main/
```
### Sync Changes Between Worktrees
Changes are automatically synced through git:
```bash
# In worktree
git add .
git commit -m "feat: implement feature"
git push
# In main directory
git pull
# Changes are now available
```
## Common Workflows
### 1. Autonomous Claude with Task Master
**Setup:**
```bash
./scripts/create-worktree.sh auto/claude-work
cd ../claude-task-master-worktrees/auto-claude-work/
```
**Run:**
```bash
# Copy the autonomous script
cp ../claude-task-master/run-autonomous-tasks.sh .
# Run Claude autonomously
./run-autonomous-tasks.sh
```
**Monitor from main directory:**
```bash
# In another terminal, in main directory
watch -n 5 "task-master list"
```
### 2. Code Review Workflow
**Main directory:**
```bash
# You write code
cursor .
git add .
git commit -m "feat: new feature"
```
**Worktree:**
```bash
cd ../claude-task-master-worktrees/auto-main/
git pull
# Have Claude review
claude -p "Review the latest commit and suggest improvements"
```
### 3. Parallel Feature Development
**Worktree 1 (Backend):**
```bash
./scripts/create-worktree.sh backend/api
cd ../claude-task-master-worktrees/backend-api/
cursor .
# Work on API
```
**Worktree 2 (Frontend):**
```bash
./scripts/create-worktree.sh frontend/ui
cd ../claude-task-master-worktrees/frontend-ui/
windsurf .
# Work on UI
```
**Main directory:**
```bash
# Monitor and merge
git log --all --graph --oneline
```
## Tips
### 1. Branch Naming Convention
Use prefixes to organize:
- `auto/*` - For autonomous AI work
- `cursor/*` - For Cursor-specific features
- `claude/*` - For Claude-specific features
- `review/*` - For code review worktrees
### 2. Commit Often in Worktrees
Worktrees make it easy to try things:
```bash
# In worktree
git commit -m "experiment: trying approach X"
# If it doesn't work, just delete the worktree
git worktree remove .
```
### 3. Use Different npm Dependencies
Each worktree can have different `node_modules`:
```bash
# Main directory
npm install
# Worktree (different dependencies)
cd ../claude-task-master-worktrees/auto-main/
npm install
# Installs independently
```
### 4. .env Files
Each worktree can have its own `.env`:
```bash
# Main directory
echo "API_URL=http://localhost:3000" > .env
# Worktree
cd ../claude-task-master-worktrees/auto-main/
echo "API_URL=http://localhost:4000" > .env
# Different config!
```
## Cleanup
### Remove All Worktrees
```bash
# List and manually remove
./scripts/list-worktrees.sh
# Remove each one
git worktree remove ../claude-task-master-worktrees/auto-main/
git worktree remove ../claude-task-master-worktrees/feature-x/
# Or remove all at once (careful!)
rm -rf ../claude-task-master-worktrees/
git worktree prune # Clean up git's worktree metadata
```
### Delete Remote Branches
```bash
# After merging/done with branches
git branch -d auto/claude-work
git push origin --delete auto/claude-work
```
## Troubleshooting
### "Cannot create worktree: already exists"
```bash
# Remove the existing worktree first
git worktree remove ../claude-task-master-worktrees/auto-main/
```
### "Branch already checked out"
Git won't let you check out the same branch in multiple worktrees:
```bash
# Use a different branch name
./scripts/create-worktree.sh auto/main-2
```
### Changes Not Syncing
Worktrees don't auto-sync files. Use git:
```bash
# In worktree with changes
git add .
git commit -m "changes"
git push
# In other worktree
git pull
```
### npm install Fails
Each worktree needs its own `node_modules`:
```bash
cd ../claude-task-master-worktrees/auto-main/
npm install
```
## Comparison to Docker
| Feature | Git Worktrees | Docker |
|---------|---------------|--------|
| Setup time | Instant | Minutes (build) |
| Disk usage | Minimal (shared .git) | GBs per container |
| Editor support | Native (any editor) | Limited (need special setup) |
| File sync | Via git | Via volumes (can be slow) |
| Resource usage | None (native) | RAM/CPU overhead |
| Complexity | Simple (just git) | Complex (Dockerfile, compose, etc.) |
| npm install | Per worktree | Per container |
| AI editor support | ✅ All editors work | ⚠️ Need web-based or special config |
**TL;DR: Worktrees are simpler, faster, and more flexible for this use case.**
---
## Summary
```bash
# 1. Create worktree
./scripts/create-worktree.sh auto/claude-work
# 2. Open in AI editor
cd ../claude-task-master-worktrees/auto-claude-work/
cursor . # or claude, windsurf, code, etc.
# 3. Work in parallel
# Main directory: You work
# Worktree: AI works
# No conflicts!
```
**Simple, fast, editor-agnostic.** 🚀
```
--------------------------------------------------------------------------------
/src/task-master.js:
--------------------------------------------------------------------------------
```javascript
/**
* task-master.js
* This module provides a centralized path management system for the Task Master application.
* It exports the TaskMaster class and the initTaskMaster factory function to create a single,
* authoritative source for all critical file and directory paths, resolving circular dependencies.
*/
import path from 'path';
import fs from 'fs';
import {
TASKMASTER_DIR,
TASKMASTER_TASKS_FILE,
LEGACY_TASKS_FILE,
TASKMASTER_DOCS_DIR,
TASKMASTER_REPORTS_DIR,
TASKMASTER_CONFIG_FILE,
LEGACY_CONFIG_FILE,
COMPLEXITY_REPORT_FILE
} from './constants/paths.js';
import { findProjectRoot } from './utils/path-utils.js';
/**
* TaskMaster class manages all the paths for the application.
* An instance of this class is created by the initTaskMaster function.
*/
export class TaskMaster {
#paths;
#tag;
/**
* The constructor is intended to be used only by the initTaskMaster factory function.
* @param {object} paths - A pre-resolved object of all application paths.
* @param {string|undefined} tag - The current tag.
*/
constructor(paths, tag) {
this.#paths = Object.freeze({ ...paths });
this.#tag = tag;
}
/**
* @returns {string|null} The absolute path to the project root.
*/
getProjectRoot() {
return this.#paths.projectRoot;
}
/**
* @returns {string|null} The absolute path to the .taskmaster directory.
*/
getTaskMasterDir() {
return this.#paths.taskMasterDir;
}
/**
* @returns {string|null} The absolute path to the tasks.json file.
*/
getTasksPath() {
return this.#paths.tasksPath;
}
/**
* @returns {string|null} The absolute path to the PRD file.
*/
getPrdPath() {
return this.#paths.prdPath;
}
/**
* @returns {string|null} The absolute path to the complexity report.
*/
getComplexityReportPath() {
if (this.#paths.complexityReportPath) {
return this.#paths.complexityReportPath;
}
const complexityReportFile =
this.getCurrentTag() !== 'master'
? COMPLEXITY_REPORT_FILE.replace(
'.json',
`_${this.getCurrentTag()}.json`
)
: COMPLEXITY_REPORT_FILE;
return path.join(this.#paths.projectRoot, complexityReportFile);
}
/**
* @returns {string|null} The absolute path to the config.json file.
*/
getConfigPath() {
return this.#paths.configPath;
}
/**
* @returns {string|null} The absolute path to the state.json file.
*/
getStatePath() {
return this.#paths.statePath;
}
/**
* @returns {object} A frozen object containing all resolved paths.
*/
getAllPaths() {
return this.#paths;
}
/**
* Gets the current tag from state.json or falls back to defaultTag from config
* @returns {string} The current tag name
*/
getCurrentTag() {
if (this.#tag) {
return this.#tag;
}
try {
// Try to read current tag from state.json using fs directly
if (fs.existsSync(this.#paths.statePath)) {
const rawState = fs.readFileSync(this.#paths.statePath, 'utf8');
const stateData = JSON.parse(rawState);
if (stateData && stateData.currentTag) {
return stateData.currentTag;
}
}
} catch (error) {
// Ignore errors, fall back to default
}
// Fall back to defaultTag from config using fs directly
try {
if (fs.existsSync(this.#paths.configPath)) {
const rawConfig = fs.readFileSync(this.#paths.configPath, 'utf8');
const configData = JSON.parse(rawConfig);
if (configData && configData.global && configData.global.defaultTag) {
return configData.global.defaultTag;
}
}
} catch (error) {
// Ignore errors, use hardcoded default
}
// Final fallback
return 'master';
}
}
/**
* Initializes a TaskMaster instance with resolved paths.
* This function centralizes path resolution logic.
*
* @param {object} [overrides={}] - An object with possible path overrides.
* @param {string} [overrides.projectRoot]
* @param {string} [overrides.tasksPath]
* @param {string} [overrides.prdPath]
* @param {string} [overrides.complexityReportPath]
* @param {string} [overrides.configPath]
* @param {string} [overrides.statePath]
* @param {string} [overrides.tag]
* @returns {TaskMaster} An initialized TaskMaster instance.
*/
export function initTaskMaster(overrides = {}) {
const resolvePath = (
pathType,
override,
defaultPaths = [],
basePath = null,
createParentDirs = false
) => {
if (typeof override === 'string') {
const resolvedPath = path.isAbsolute(override)
? override
: path.resolve(basePath || process.cwd(), override);
if (createParentDirs) {
// For output paths, create parent directory if it doesn't exist
const parentDir = path.dirname(resolvedPath);
if (!fs.existsSync(parentDir)) {
try {
fs.mkdirSync(parentDir, { recursive: true });
} catch (error) {
throw new Error(
`Could not create directory for ${pathType}: ${parentDir}. Error: ${error.message}`
);
}
}
} else {
// Original validation logic
if (!fs.existsSync(resolvedPath)) {
throw new Error(
`${pathType} override path does not exist: ${resolvedPath}`
);
}
}
return resolvedPath;
}
if (override === true) {
// Required path - search defaults and fail if not found
for (const defaultPath of defaultPaths) {
const fullPath = path.isAbsolute(defaultPath)
? defaultPath
: path.join(basePath || process.cwd(), defaultPath);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
throw new Error(
`Required ${pathType} not found. Searched: ${defaultPaths.join(', ')}`
);
}
// Optional path (override === false/undefined) - search defaults, return null if not found
for (const defaultPath of defaultPaths) {
const fullPath = path.isAbsolute(defaultPath)
? defaultPath
: path.join(basePath || process.cwd(), defaultPath);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return null;
};
const paths = {};
// Project Root
if (overrides.projectRoot) {
const resolvedOverride = path.resolve(overrides.projectRoot);
if (!fs.existsSync(resolvedOverride)) {
throw new Error(
`Project root override path does not exist: ${resolvedOverride}`
);
}
const hasTaskmasterDir = fs.existsSync(
path.join(resolvedOverride, TASKMASTER_DIR)
);
const hasLegacyConfig = fs.existsSync(
path.join(resolvedOverride, LEGACY_CONFIG_FILE)
);
if (!hasTaskmasterDir && !hasLegacyConfig) {
throw new Error(
`Project root override is not a valid taskmaster project: ${resolvedOverride}`
);
}
paths.projectRoot = resolvedOverride;
} else {
// findProjectRoot now always returns a value (fallback to cwd)
paths.projectRoot = findProjectRoot();
}
// TaskMaster Directory
if ('taskMasterDir' in overrides) {
paths.taskMasterDir = resolvePath(
'taskmaster directory',
overrides.taskMasterDir,
[TASKMASTER_DIR],
paths.projectRoot
);
} else {
paths.taskMasterDir = resolvePath(
'taskmaster directory',
false,
[TASKMASTER_DIR],
paths.projectRoot
);
}
// Always set default paths first
// These can be overridden below if needed
paths.configPath = path.join(paths.projectRoot, TASKMASTER_CONFIG_FILE);
paths.statePath = path.join(
paths.taskMasterDir || path.join(paths.projectRoot, TASKMASTER_DIR),
'state.json'
);
paths.tasksPath = path.join(paths.projectRoot, TASKMASTER_TASKS_FILE);
// Handle overrides - only validate/resolve if explicitly provided
if ('configPath' in overrides) {
paths.configPath = resolvePath(
'config file',
overrides.configPath,
[TASKMASTER_CONFIG_FILE, LEGACY_CONFIG_FILE],
paths.projectRoot
);
}
if ('statePath' in overrides) {
paths.statePath = resolvePath(
'state file',
overrides.statePath,
['state.json'],
paths.taskMasterDir
);
}
if ('tasksPath' in overrides) {
paths.tasksPath = resolvePath(
'tasks file',
overrides.tasksPath,
[TASKMASTER_TASKS_FILE, LEGACY_TASKS_FILE],
paths.projectRoot
);
}
if ('prdPath' in overrides) {
paths.prdPath = resolvePath(
'PRD file',
overrides.prdPath,
[
path.join(TASKMASTER_DOCS_DIR, 'PRD.md'),
path.join(TASKMASTER_DOCS_DIR, 'prd.md'),
path.join(TASKMASTER_DOCS_DIR, 'PRD.txt'),
path.join(TASKMASTER_DOCS_DIR, 'prd.txt'),
path.join('scripts', 'PRD.md'),
path.join('scripts', 'prd.md'),
path.join('scripts', 'PRD.txt'),
path.join('scripts', 'prd.txt'),
'PRD.md',
'prd.md',
'PRD.txt',
'prd.txt'
],
paths.projectRoot
);
}
if ('complexityReportPath' in overrides) {
paths.complexityReportPath = resolvePath(
'complexity report',
overrides.complexityReportPath,
[
path.join(TASKMASTER_REPORTS_DIR, 'task-complexity-report.json'),
path.join(TASKMASTER_REPORTS_DIR, 'complexity-report.json'),
path.join('scripts', 'task-complexity-report.json'),
path.join('scripts', 'complexity-report.json'),
'task-complexity-report.json',
'complexity-report.json'
],
paths.projectRoot,
true // Enable parent directory creation for output paths
);
}
return new TaskMaster(paths, overrides.tag);
}
```
--------------------------------------------------------------------------------
/tests/unit/profiles/vscode-integration.test.js:
--------------------------------------------------------------------------------
```javascript
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import os from 'os';
// Mock the schema integration functions to avoid chalk issues
const mockSetupSchemaIntegration = jest.fn();
import { vscodeProfile } from '../../../src/profiles/vscode.js';
// Mock external modules
jest.mock('child_process', () => ({
execSync: jest.fn()
}));
// Mock fs/promises
const mockFsPromises = {
mkdir: jest.fn(),
access: jest.fn(),
copyFile: jest.fn(),
readFile: jest.fn(),
writeFile: jest.fn()
};
jest.mock('fs/promises', () => mockFsPromises);
// Mock console methods
jest.mock('console', () => ({
log: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
clear: jest.fn()
}));
describe('VS Code Integration', () => {
let tempDir;
beforeEach(() => {
jest.clearAllMocks();
// Create a temporary directory for testing
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
// Spy on fs methods
jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
if (filePath.toString().includes('mcp.json')) {
return JSON.stringify({
mcpServers: {
'task-master-ai': {
command: 'node',
args: ['mcp-server/src/index.js']
}
}
});
}
if (filePath.toString().includes('instructions')) {
return 'VS Code instruction content';
}
return '{}';
});
jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
});
afterEach(() => {
// Clean up the temporary directory
try {
fs.rmSync(tempDir, { recursive: true, force: true });
} catch (err) {
console.error(`Error cleaning up: ${err.message}`);
}
});
// Test function that simulates the createProjectStructure behavior for VS Code files
function mockCreateVSCodeStructure() {
// Create .vscode directory for MCP configuration
fs.mkdirSync(path.join(tempDir, '.vscode'), { recursive: true });
// Create .github/instructions directory for VS Code custom instructions
fs.mkdirSync(path.join(tempDir, '.github', 'instructions'), {
recursive: true
});
fs.mkdirSync(path.join(tempDir, '.github', 'instructions', 'taskmaster'), {
recursive: true
});
// Create MCP configuration file
const mcpConfig = {
mcpServers: {
'task-master-ai': {
command: 'node',
args: ['mcp-server/src/index.js'],
env: {
PROJECT_ROOT: process.cwd()
}
}
}
};
fs.writeFileSync(
path.join(tempDir, '.vscode', 'mcp.json'),
JSON.stringify(mcpConfig, null, 2)
);
// Create sample instruction files
const instructionFiles = [
'vscode_rules.md',
'dev_workflow.md',
'self_improve.md'
];
for (const file of instructionFiles) {
const content = `---
description: VS Code instruction for ${file}
applyTo: "**/*.ts,**/*.tsx,**/*.js,**/*.jsx"
alwaysApply: true
---
# ${file.replace('.md', '').replace('_', ' ').toUpperCase()}
This is a VS Code custom instruction file.`;
fs.writeFileSync(
path.join(tempDir, '.github', 'instructions', file),
content
);
}
// Create taskmaster subdirectory with additional instructions
const taskmasterFiles = ['taskmaster.md', 'commands.md', 'architecture.md'];
for (const file of taskmasterFiles) {
const content = `---
description: Task Master specific instruction for ${file}
applyTo: "**/*.ts,**/*.js"
alwaysApply: true
---
# ${file.replace('.md', '').toUpperCase()}
Task Master specific VS Code instruction.`;
fs.writeFileSync(
path.join(tempDir, '.github', 'instructions', 'taskmaster', file),
content
);
}
}
test('creates all required VS Code directories', () => {
// Act
mockCreateVSCodeStructure();
// Assert - .vscode directory for MCP config
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.vscode'), {
recursive: true
});
// Assert - .github/instructions directory for custom instructions
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.github', 'instructions'),
{ recursive: true }
);
// Assert - taskmaster subdirectory
expect(fs.mkdirSync).toHaveBeenCalledWith(
path.join(tempDir, '.github', 'instructions', 'taskmaster'),
{ recursive: true }
);
});
test('creates VS Code MCP configuration file', () => {
// Act
mockCreateVSCodeStructure();
// Assert
const expectedMcpPath = path.join(tempDir, '.vscode', 'mcp.json');
expect(fs.writeFileSync).toHaveBeenCalledWith(
expectedMcpPath,
expect.stringContaining('task-master-ai')
);
});
test('creates VS Code instruction files with applyTo patterns', () => {
// Act
mockCreateVSCodeStructure();
// Assert main instruction files
const mainInstructionFiles = [
'vscode_rules.md',
'dev_workflow.md',
'self_improve.md'
];
for (const file of mainInstructionFiles) {
const expectedPath = path.join(tempDir, '.github', 'instructions', file);
expect(fs.writeFileSync).toHaveBeenCalledWith(
expectedPath,
expect.stringContaining('applyTo:')
);
}
});
test('creates taskmaster specific instruction files', () => {
// Act
mockCreateVSCodeStructure();
// Assert taskmaster subdirectory files
const taskmasterFiles = ['taskmaster.md', 'commands.md', 'architecture.md'];
for (const file of taskmasterFiles) {
const expectedPath = path.join(
tempDir,
'.github',
'instructions',
'taskmaster',
file
);
expect(fs.writeFileSync).toHaveBeenCalledWith(
expectedPath,
expect.stringContaining('applyTo:')
);
}
});
test('VS Code instruction files use applyTo instead of globs', () => {
// Act
mockCreateVSCodeStructure();
// Get all the writeFileSync calls for .md files
const mdFileWrites = fs.writeFileSync.mock.calls.filter((call) =>
call[0].toString().endsWith('.md')
);
// Assert that all .md files contain applyTo and not globs
for (const writeCall of mdFileWrites) {
const content = writeCall[1];
expect(content).toContain('applyTo:');
expect(content).not.toContain('globs:');
}
});
test('MCP configuration includes correct structure for VS Code', () => {
// Act
mockCreateVSCodeStructure();
// Get the MCP config write call
const mcpConfigWrite = fs.writeFileSync.mock.calls.find((call) =>
call[0].toString().includes('mcp.json')
);
expect(mcpConfigWrite).toBeDefined();
const mcpContent = mcpConfigWrite[1];
const mcpConfig = JSON.parse(mcpContent);
// Assert MCP structure
expect(mcpConfig).toHaveProperty('mcpServers');
expect(mcpConfig.mcpServers).toHaveProperty('task-master-ai');
expect(mcpConfig.mcpServers['task-master-ai']).toHaveProperty(
'command',
'node'
);
expect(mcpConfig.mcpServers['task-master-ai']).toHaveProperty('args');
expect(mcpConfig.mcpServers['task-master-ai'].args).toContain(
'mcp-server/src/index.js'
);
});
test('directory structure follows VS Code conventions', () => {
// Act
mockCreateVSCodeStructure();
// Assert the specific directory structure VS Code expects
const expectedDirs = [
path.join(tempDir, '.vscode'),
path.join(tempDir, '.github', 'instructions'),
path.join(tempDir, '.github', 'instructions', 'taskmaster')
];
for (const dir of expectedDirs) {
expect(fs.mkdirSync).toHaveBeenCalledWith(dir, { recursive: true });
}
});
test('instruction files contain VS Code specific formatting', () => {
// Act
mockCreateVSCodeStructure();
// Get a sample instruction file write
const instructionWrite = fs.writeFileSync.mock.calls.find((call) =>
call[0].toString().includes('vscode_rules.md')
);
expect(instructionWrite).toBeDefined();
const content = instructionWrite[1];
// Assert VS Code specific patterns
expect(content).toContain('---'); // YAML frontmatter
expect(content).toContain('description:');
expect(content).toContain('applyTo:');
expect(content).toContain('alwaysApply:');
expect(content).toContain('**/*.ts'); // File patterns in quotes
});
describe('Schema Integration', () => {
beforeEach(() => {
jest.clearAllMocks();
// Replace the onAddRulesProfile function with our mock
vscodeProfile.onAddRulesProfile = mockSetupSchemaIntegration;
});
test('setupSchemaIntegration is called with project root', async () => {
// Arrange
mockSetupSchemaIntegration.mockResolvedValue();
// Act
await vscodeProfile.onAddRulesProfile(tempDir);
// Assert
expect(mockSetupSchemaIntegration).toHaveBeenCalledWith(tempDir);
});
test('schema integration function exists and is callable', () => {
// Assert that the VS Code profile has the schema integration function
expect(vscodeProfile.onAddRulesProfile).toBeDefined();
expect(typeof vscodeProfile.onAddRulesProfile).toBe('function');
});
test('schema integration handles errors gracefully', async () => {
// Arrange
mockSetupSchemaIntegration.mockRejectedValue(
new Error('Schema setup failed')
);
// Act & Assert - Should propagate the error
await expect(vscodeProfile.onAddRulesProfile(tempDir)).rejects.toThrow(
'Schema setup failed'
);
});
});
});
```
--------------------------------------------------------------------------------
/apps/docs/archive/ai-client-utils-example.mdx:
--------------------------------------------------------------------------------
```markdown
---
title: "AI Client Utilities for MCP Tools"
description: "This document provides examples of how to use the new AI client utilities with AsyncOperationManager in MCP tools."
---
## Examples
<AccordionGroup>
<Accordion title="Basic Usage with Direct Functions">
```javascript
// In your direct function implementation:
import {
getAnthropicClientForMCP,
getModelConfig,
handleClaudeError
} from '../utils/ai-client-utils.js';
export async function someAiOperationDirect(args, log, context) {
try {
// Initialize Anthropic client with session from context
const client = getAnthropicClientForMCP(context.session, log);
// Get model configuration with defaults or session overrides
const modelConfig = getModelConfig(context.session);
// Make API call with proper error handling
try {
const response = await client.messages.create({
model: modelConfig.model,
max_tokens: modelConfig.maxTokens,
temperature: modelConfig.temperature,
messages: [{ role: 'user', content: 'Your prompt here' }]
});
return {
success: true,
data: response
};
} catch (apiError) {
// Use helper to get user-friendly error message
const friendlyMessage = handleClaudeError(apiError);
return {
success: false,
error: {
code: 'AI_API_ERROR',
message: friendlyMessage
}
};
}
} catch (error) {
// Handle client initialization errors
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: error.message
}
};
}
}
```
</Accordion>
<Accordion title="Integration with AsyncOperationManager">
```javascript
// In your MCP tool implementation:
import {
AsyncOperationManager,
StatusCodes
} from '../../utils/async-operation-manager.js';
import { someAiOperationDirect } from '../../core/direct-functions/some-ai-operation.js';
export async function someAiOperation(args, context) {
const { session, mcpLog } = context;
const log = mcpLog || console;
try {
// Create operation description
const operationDescription = `AI operation: ${args.someParam}`;
// Start async operation
const operation = AsyncOperationManager.createOperation(
operationDescription,
async (reportProgress) => {
try {
// Initial progress report
reportProgress({
progress: 0,
status: 'Starting AI operation...'
});
// Call direct function with session and progress reporting
const result = await someAiOperationDirect(args, log, {
reportProgress,
mcpLog: log,
session
});
// Final progress update
reportProgress({
progress: 100,
status: result.success ? 'Operation completed' : 'Operation failed',
result: result.data,
error: result.error
});
return result;
} catch (error) {
// Handle errors in the operation
reportProgress({
progress: 100,
status: 'Operation failed',
error: {
message: error.message,
code: error.code || 'OPERATION_FAILED'
}
});
throw error;
}
}
);
// Return immediate response with operation ID
return {
status: StatusCodes.ACCEPTED,
body: {
success: true,
message: 'Operation started',
operationId: operation.id
}
};
} catch (error) {
// Handle errors in the MCP tool
log.error(`Error in someAiOperation: ${error.message}`);
return {
status: StatusCodes.INTERNAL_SERVER_ERROR,
body: {
success: false,
error: {
code: 'OPERATION_FAILED',
message: error.message
}
}
};
}
}
```
</Accordion>
<Accordion title="Using Research Capabilities with Perplexity">
```javascript
// In your direct function:
import {
getPerplexityClientForMCP,
getBestAvailableAIModel
} from '../utils/ai-client-utils.js';
export async function researchOperationDirect(args, log, context) {
try {
// Get the best AI model for this operation based on needs
const { type, client } = await getBestAvailableAIModel(
context.session,
{ requiresResearch: true },
log
);
// Report which model we're using
if (context.reportProgress) {
await context.reportProgress({
progress: 10,
status: `Using ${type} model for research...`
});
}
// Make API call based on the model type
if (type === 'perplexity') {
// Call Perplexity
const response = await client.chat.completions.create({
model: context.session?.env?.PERPLEXITY_MODEL || 'sonar-medium-online',
messages: [{ role: 'user', content: args.researchQuery }],
temperature: 0.1
});
return {
success: true,
data: response.choices[0].message.content
};
} else {
// Call Claude as fallback
// (Implementation depends on specific needs)
// ...
}
} catch (error) {
// Handle errors
return {
success: false,
error: {
code: 'RESEARCH_ERROR',
message: error.message
}
};
}
}
```
</Accordion>
<Accordion title="Model Configuration Override">
```javascript
// In your direct function:
import { getModelConfig } from '../utils/ai-client-utils.js';
// Using custom defaults for a specific operation
const operationDefaults = {
model: 'claude-3-haiku-20240307', // Faster, smaller model
maxTokens: 1000, // Lower token limit
temperature: 0.2 // Lower temperature for more deterministic output
};
// Get model config with operation-specific defaults
const modelConfig = getModelConfig(context.session, operationDefaults);
// Now use modelConfig in your API calls
const response = await client.messages.create({
model: modelConfig.model,
max_tokens: modelConfig.maxTokens,
temperature: modelConfig.temperature
// Other parameters...
});
```
</Accordion>
</AccordionGroup>
## Best Practices
<AccordionGroup>
<Accordion title="Error Handling">
- Always use try/catch blocks around both client initialization and API calls
- Use `handleClaudeError` to provide user-friendly error messages
- Return standardized error objects with code and message
</Accordion>
<Accordion title="Progress Reporting">
- Report progress at key points (starting, processing, completing)
- Include meaningful status messages
- Include error details in progress reports when failures occur
</Accordion>
<Accordion title="Session Handling">
- Always pass the session from the context to the AI client getters
- Use `getModelConfig` to respect user settings from session
</Accordion>
<Accordion title="Model Selection">
- Use `getBestAvailableAIModel` when you need to select between different models
- Set `requiresResearch: true` when you need Perplexity capabilities
</Accordion>
<Accordion title="AsyncOperationManager Integration">
- Create descriptive operation names
- Handle all errors within the operation function
- Return standardized results from direct functions
- Return immediate responses with operation IDs
</Accordion>
</AccordionGroup>
```
--------------------------------------------------------------------------------
/.taskmaster/reports/task-complexity-report_tm-core-phase-1.json:
--------------------------------------------------------------------------------
```json
{
"meta": {
"generatedAt": "2025-08-06T12:39:03.250Z",
"tasksAnalyzed": 8,
"totalTasks": 11,
"analysisCount": 8,
"thresholdScore": 5,
"projectName": "Taskmaster",
"usedResearch": false
},
"complexityAnalysis": [
{
"taskId": 118,
"taskTitle": "Create AI Provider Base Architecture",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Break down the implementation of BaseProvider abstract TypeScript class into subtasks focusing on: 1) Converting existing JavaScript base-provider.js to TypeScript with proper interface definitions, 2) Implementing the Template Method pattern with abstract methods, 3) Adding comprehensive error handling and retry logic with exponential backoff, 4) Creating proper TypeScript types for all method signatures and options, 5) Setting up comprehensive unit tests with MockProvider. Consider that the existing codebase uses JavaScript ES modules and Vercel AI SDK, so the TypeScript implementation needs to maintain compatibility while adding type safety.",
"reasoning": "This task requires significant architectural work including converting existing JavaScript code to TypeScript, creating new interfaces, implementing design patterns, and ensuring backward compatibility. The existing base-provider.js already implements a sophisticated provider pattern using Vercel AI SDK, so the TypeScript conversion needs careful consideration of type definitions and maintaining existing functionality."
},
{
"taskId": 119,
"taskTitle": "Implement Provider Factory with Dynamic Imports",
"complexityScore": 5,
"recommendedSubtasks": 5,
"expansionPrompt": "Break down the Provider Factory implementation into: 1) Creating the ProviderFactory class structure with proper TypeScript typing, 2) Implementing the switch statement for provider selection logic, 3) Adding dynamic imports for each provider to enable tree-shaking, 4) Handling provider instantiation with configuration passing, 5) Implementing comprehensive error handling for module loading failures. Note that the existing codebase already has a provider selection mechanism in the JavaScript files, so ensure the factory pattern integrates smoothly with existing infrastructure.",
"reasoning": "This is a moderate complexity task that involves creating a factory pattern with dynamic imports. The existing codebase already has provider management logic, so the main complexity is in creating a clean TypeScript implementation with proper dynamic imports while maintaining compatibility with the existing JavaScript module system."
},
{
"taskId": 120,
"taskTitle": "Implement Anthropic Provider",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "Implement the AnthropicProvider class in stages: 1) Set up the class structure extending BaseProvider with proper TypeScript imports and type definitions, 2) Implement constructor with Anthropic SDK client initialization and configuration handling, 3) Implement generateCompletion method with proper message format transformation and error handling, 4) Add token calculation methods and utility functions (getName, getModel, getDefaultModel), 5) Implement comprehensive error handling with custom error wrapping and type exports. The existing anthropic.js provider can serve as a reference but needs to be reimplemented to extend the new TypeScript BaseProvider.",
"reasoning": "This task involves integrating with an external SDK (@anthropic-ai/sdk) and implementing all abstract methods from BaseProvider. The existing JavaScript implementation provides a good reference, but the TypeScript version needs proper type definitions, error handling, and must work with the new abstract base class architecture."
},
{
"taskId": 121,
"taskTitle": "Create Prompt Builder and Task Parser",
"complexityScore": 8,
"recommendedSubtasks": 5,
"expansionPrompt": "Implement PromptBuilder and TaskParser with focus on: 1) Creating PromptBuilder class with template methods for building structured prompts with JSON format instructions, 2) Implementing TaskParser class structure with dependency injection of IAIProvider and IConfiguration, 3) Implementing parsePRD method with file reading, prompt generation, and AI provider integration, 4) Adding task enrichment logic with metadata, validation, and structure verification, 5) Implementing comprehensive error handling for all failure scenarios including file I/O, AI provider errors, and JSON parsing. The existing parse-prd.js provides complex logic that needs to be reimplemented with proper TypeScript types and cleaner architecture.",
"reasoning": "This is a complex task that involves multiple components working together: file I/O, AI provider integration, JSON parsing, and data validation. The existing parse-prd.js implementation is quite sophisticated with Zod schemas and complex task processing logic that needs to be reimplemented in TypeScript with proper separation of concerns."
},
{
"taskId": 122,
"taskTitle": "Implement Configuration Management",
"complexityScore": 6,
"recommendedSubtasks": 5,
"expansionPrompt": "Create ConfigManager implementation focusing on: 1) Setting up Zod validation schema that matches the IConfiguration interface structure, 2) Implementing ConfigManager constructor with default values merging and storage initialization, 3) Creating validate method with Zod schema parsing and user-friendly error transformation, 4) Implementing type-safe get method using TypeScript generics and keyof operator, 5) Adding getAll method and ensuring proper immutability and module exports. The existing config-manager.js has complex configuration loading logic that can inform the TypeScript implementation but needs cleaner architecture.",
"reasoning": "This task involves creating a configuration management system with validation using Zod. The existing JavaScript config-manager.js is quite complex with multiple configuration sources, defaults, and validation logic. The TypeScript version needs to provide a cleaner API while maintaining the flexibility of the current system."
},
{
"taskId": 123,
"taskTitle": "Create Utility Functions and Error Handling",
"complexityScore": 4,
"recommendedSubtasks": 5,
"expansionPrompt": "Implement utilities and error handling in stages: 1) Create ID generation module with generateTaskId and generateSubtaskId functions using proper random generation, 2) Implement base TaskMasterError class extending Error with proper TypeScript typing, 3) Add error sanitization methods to prevent sensitive data exposure in production, 4) Implement development-only logging with environment detection, 5) Create specialized error subclasses (FileNotFoundError, ParseError, ValidationError, APIError) with appropriate error codes and formatting.",
"reasoning": "This is a relatively straightforward task involving utility functions and error class hierarchies. The main complexity is in ensuring proper error sanitization for production use and creating a well-structured error hierarchy that can be used throughout the application."
},
{
"taskId": 124,
"taskTitle": "Implement TaskMasterCore Facade",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Build TaskMasterCore facade implementation: 1) Create class structure with proper TypeScript imports and type definitions for all subsystem interfaces, 2) Implement initialize method for lazy loading AI provider and parser instances based on configuration, 3) Create parsePRD method that coordinates parser, AI provider, and storage subsystems, 4) Implement getTasks and other facade methods for task retrieval and management, 5) Create createTaskMaster factory function and set up all module exports including type re-exports. Ensure proper ESM compatibility with .js extensions in imports.",
"reasoning": "This is a complex integration task that brings together all the other components into a cohesive facade. It requires understanding of the facade pattern, proper dependency management, lazy initialization, and careful module export structure for the public API."
},
{
"taskId": 125,
"taskTitle": "Create Placeholder Providers and Complete Testing",
"complexityScore": 5,
"recommendedSubtasks": 5,
"expansionPrompt": "Complete the implementation with placeholders and testing: 1) Create OpenAIProvider placeholder class extending BaseProvider with 'not yet implemented' errors, 2) Create GoogleProvider placeholder class with similar structure, 3) Implement MockProvider in tests/mocks directory with configurable responses and behavior simulation, 4) Write comprehensive unit tests for TaskParser covering all methods and edge cases, 5) Create integration tests for the complete parse-prd workflow ensuring 80% code coverage. Follow kebab-case naming convention for test files.",
"reasoning": "This task involves creating placeholder implementations and a comprehensive test suite. While the placeholder providers are simple, creating a good MockProvider and comprehensive tests requires understanding the entire system architecture and ensuring all edge cases are covered."
}
]
}
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/services/environment-config-provider.service.spec.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Unit tests for EnvironmentConfigProvider service
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { EnvironmentConfigProvider } from './environment-config-provider.service.js';
describe('EnvironmentConfigProvider', () => {
let provider: EnvironmentConfigProvider;
const originalEnv = { ...process.env };
beforeEach(() => {
// Clear all TASKMASTER_ env vars
Object.keys(process.env).forEach((key) => {
if (key.startsWith('TASKMASTER_')) {
delete process.env[key];
}
});
provider = new EnvironmentConfigProvider();
});
afterEach(() => {
// Restore original environment
process.env = { ...originalEnv };
});
describe('loadConfig', () => {
it('should load configuration from environment variables', () => {
process.env.TASKMASTER_STORAGE_TYPE = 'api';
process.env.TASKMASTER_API_ENDPOINT = 'https://api.example.com';
process.env.TASKMASTER_MODEL_MAIN = 'gpt-4';
const config = provider.loadConfig();
expect(config).toEqual({
storage: {
type: 'api',
apiEndpoint: 'https://api.example.com'
},
models: {
main: 'gpt-4'
}
});
});
it('should return empty object when no env vars are set', () => {
const config = provider.loadConfig();
expect(config).toEqual({});
});
it('should skip runtime state variables', () => {
process.env.TASKMASTER_TAG = 'feature-branch';
process.env.TASKMASTER_MODEL_MAIN = 'claude-3';
const config = provider.loadConfig();
expect(config).toEqual({
models: { main: 'claude-3' }
});
expect(config).not.toHaveProperty('activeTag');
});
it('should validate storage type values', () => {
// Mock console.warn to check validation
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
process.env.TASKMASTER_STORAGE_TYPE = 'invalid';
const config = provider.loadConfig();
expect(config).toEqual({});
expect(warnSpy).toHaveBeenCalledWith(
'Invalid value for TASKMASTER_STORAGE_TYPE: invalid'
);
warnSpy.mockRestore();
});
it('should accept valid storage type values', () => {
process.env.TASKMASTER_STORAGE_TYPE = 'file';
let config = provider.loadConfig();
expect(config.storage?.type).toBe('file');
process.env.TASKMASTER_STORAGE_TYPE = 'api';
provider = new EnvironmentConfigProvider(); // Reset provider
config = provider.loadConfig();
expect(config.storage?.type).toBe('api');
});
it('should handle nested configuration paths', () => {
process.env.TASKMASTER_MODEL_MAIN = 'model1';
process.env.TASKMASTER_MODEL_RESEARCH = 'model2';
process.env.TASKMASTER_MODEL_FALLBACK = 'model3';
const config = provider.loadConfig();
expect(config).toEqual({
models: {
main: 'model1',
research: 'model2',
fallback: 'model3'
}
});
});
it('should handle custom response language', () => {
process.env.TASKMASTER_RESPONSE_LANGUAGE = 'Spanish';
const config = provider.loadConfig();
expect(config).toEqual({
custom: {
responseLanguage: 'Spanish'
}
});
});
it('should ignore empty string values', () => {
process.env.TASKMASTER_MODEL_MAIN = '';
process.env.TASKMASTER_MODEL_FALLBACK = 'fallback-model';
const config = provider.loadConfig();
expect(config).toEqual({
models: {
fallback: 'fallback-model'
}
});
});
});
describe('getRuntimeState', () => {
it('should extract runtime state variables', () => {
process.env.TASKMASTER_TAG = 'develop';
process.env.TASKMASTER_MODEL_MAIN = 'model'; // Should not be included
const state = provider.getRuntimeState();
expect(state).toEqual({
activeTag: 'develop'
});
});
it('should return empty object when no runtime state vars', () => {
process.env.TASKMASTER_MODEL_MAIN = 'model';
const state = provider.getRuntimeState();
expect(state).toEqual({});
});
});
describe('hasEnvVar', () => {
it('should return true when env var exists', () => {
process.env.TASKMASTER_MODEL_MAIN = 'test';
expect(provider.hasEnvVar('TASKMASTER_MODEL_MAIN')).toBe(true);
});
it('should return false when env var does not exist', () => {
expect(provider.hasEnvVar('TASKMASTER_NONEXISTENT')).toBe(false);
});
it('should return false for undefined values', () => {
process.env.TASKMASTER_TEST = undefined as any;
expect(provider.hasEnvVar('TASKMASTER_TEST')).toBe(false);
});
});
describe('getAllTaskmasterEnvVars', () => {
it('should return all TASKMASTER_ prefixed variables', () => {
process.env.TASKMASTER_VAR1 = 'value1';
process.env.TASKMASTER_VAR2 = 'value2';
process.env.OTHER_VAR = 'other';
process.env.TASK_MASTER = 'wrong-prefix';
const vars = provider.getAllTaskmasterEnvVars();
expect(vars).toEqual({
TASKMASTER_VAR1: 'value1',
TASKMASTER_VAR2: 'value2'
});
});
it('should return empty object when no TASKMASTER_ vars', () => {
process.env.OTHER_VAR = 'value';
const vars = provider.getAllTaskmasterEnvVars();
expect(vars).toEqual({});
});
it('should filter out undefined values', () => {
process.env.TASKMASTER_DEFINED = 'value';
process.env.TASKMASTER_UNDEFINED = undefined as any;
const vars = provider.getAllTaskmasterEnvVars();
expect(vars).toEqual({
TASKMASTER_DEFINED: 'value'
});
});
});
describe('custom mappings', () => {
it('should use custom mappings when provided', () => {
const customMappings = [{ env: 'CUSTOM_VAR', path: ['custom', 'value'] }];
const customProvider = new EnvironmentConfigProvider(customMappings);
process.env.CUSTOM_VAR = 'test-value';
const config = customProvider.loadConfig();
expect(config).toEqual({
custom: {
value: 'test-value'
}
});
});
it('should add new mapping with addMapping', () => {
process.env.NEW_MAPPING = 'new-value';
provider.addMapping({
env: 'NEW_MAPPING',
path: ['new', 'mapping']
});
const config = provider.loadConfig();
expect(config).toHaveProperty('new.mapping', 'new-value');
});
it('should return current mappings with getMappings', () => {
const mappings = provider.getMappings();
expect(mappings).toBeInstanceOf(Array);
expect(mappings.length).toBeGreaterThan(0);
// Check for some expected mappings
const envNames = mappings.map((m) => m.env);
expect(envNames).toContain('TASKMASTER_STORAGE_TYPE');
expect(envNames).toContain('TASKMASTER_MODEL_MAIN');
expect(envNames).toContain('TASKMASTER_TAG');
});
it('should return copy of mappings array', () => {
const mappings1 = provider.getMappings();
const mappings2 = provider.getMappings();
expect(mappings1).not.toBe(mappings2); // Different instances
expect(mappings1).toEqual(mappings2); // Same content
});
});
describe('validation', () => {
it('should validate values when validator is provided', () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
process.env.TASKMASTER_STORAGE_TYPE = 'database'; // Invalid
const config = provider.loadConfig();
expect(config).toEqual({});
expect(warnSpy).toHaveBeenCalledWith(
'Invalid value for TASKMASTER_STORAGE_TYPE: database'
);
warnSpy.mockRestore();
});
it('should accept values that pass validation', () => {
process.env.TASKMASTER_STORAGE_TYPE = 'file';
const config = provider.loadConfig();
expect(config.storage?.type).toBe('file');
});
it('should work with custom validators', () => {
const customProvider = new EnvironmentConfigProvider([
{
env: 'CUSTOM_NUMBER',
path: ['custom', 'number'],
validate: (v) => !isNaN(Number(v))
}
]);
process.env.CUSTOM_NUMBER = '123';
let config = customProvider.loadConfig();
expect(config.custom?.number).toBe('123');
process.env.CUSTOM_NUMBER = 'not-a-number';
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
customProvider = new EnvironmentConfigProvider([
{
env: 'CUSTOM_NUMBER',
path: ['custom', 'number'],
validate: (v) => !isNaN(Number(v))
}
]);
config = customProvider.loadConfig();
expect(config).toEqual({});
expect(warnSpy).toHaveBeenCalled();
warnSpy.mockRestore();
});
});
describe('edge cases', () => {
it('should handle special characters in values', () => {
process.env.TASKMASTER_API_ENDPOINT =
'https://api.example.com/v1?key=abc&token=xyz';
process.env.TASKMASTER_API_TOKEN = 'Bearer abc123!@#$%^&*()';
const config = provider.loadConfig();
expect(config.storage?.apiEndpoint).toBe(
'https://api.example.com/v1?key=abc&token=xyz'
);
expect(config.storage?.apiAccessToken).toBe('Bearer abc123!@#$%^&*()');
});
it('should handle whitespace in values', () => {
process.env.TASKMASTER_MODEL_MAIN = ' claude-3 ';
const config = provider.loadConfig();
// Note: We're not trimming, preserving the value as-is
expect(config.models?.main).toBe(' claude-3 ');
});
it('should handle very long values', () => {
const longValue = 'a'.repeat(10000);
process.env.TASKMASTER_API_TOKEN = longValue;
const config = provider.loadConfig();
expect(config.storage?.apiAccessToken).toBe(longValue);
});
});
});
```
--------------------------------------------------------------------------------
/apps/cli/src/commands/export.command.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Export command for exporting tasks to external systems
* Provides functionality to export tasks to Hamster briefs
*/
import {
AuthManager,
type ExportResult,
type TmCore,
type UserContext,
createTmCore
} from '@tm/core';
import chalk from 'chalk';
import { Command } from 'commander';
import inquirer from 'inquirer';
import ora, { Ora } from 'ora';
import { displayError } from '../utils/error-handler.js';
import * as ui from '../utils/ui.js';
import { getProjectRoot } from '../utils/project-root.js';
/**
* Result type from export command
*/
export interface ExportCommandResult {
success: boolean;
action: 'export' | 'validate' | 'cancelled';
result?: ExportResult;
message?: string;
}
/**
* ExportCommand extending Commander's Command class
* Handles task export to external systems
*/
export class ExportCommand extends Command {
private authManager: AuthManager;
private taskMasterCore?: TmCore;
private lastResult?: ExportCommandResult;
constructor(name?: string) {
super(name || 'export');
// Initialize auth manager
this.authManager = AuthManager.getInstance();
// Configure the command
this.description('Export tasks to external systems (e.g., Hamster briefs)');
// Add options
this.option('--org <id>', 'Organization ID to export to');
this.option('--brief <id>', 'Brief ID to export tasks to');
this.option('--tag <tag>', 'Export tasks from a specific tag');
this.option(
'--status <status>',
'Filter tasks by status (pending, in-progress, done, etc.)'
);
this.option('--exclude-subtasks', 'Exclude subtasks from export');
this.option('-y, --yes', 'Skip confirmation prompt');
// Accept optional positional argument for brief ID or Hamster URL
this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL');
// Default action
this.action(async (briefOrUrl?: string, options?: any) => {
await this.executeExport(briefOrUrl, options);
});
}
/**
* Initialize the TmCore
*/
private async initializeServices(): Promise<void> {
if (this.taskMasterCore) {
return;
}
try {
// Initialize TmCore
this.taskMasterCore = await createTmCore({
projectPath: getProjectRoot()
});
} catch (error) {
throw new Error(
`Failed to initialize services: ${(error as Error).message}`
);
}
}
/**
* Execute the export command
*/
private async executeExport(
briefOrUrl?: string,
options?: any
): Promise<void> {
let spinner: Ora | undefined;
try {
// Check authentication
const hasSession = await this.authManager.hasValidSession();
if (!hasSession) {
ui.displayError('Not authenticated. Run "tm auth login" first.');
process.exit(1);
}
// Initialize services
await this.initializeServices();
// Get current context
const context = await this.authManager.getContext();
// Determine org and brief IDs
let orgId = options?.org || context?.orgId;
let briefId = options?.brief || briefOrUrl || context?.briefId;
// If a URL/ID was provided as argument, resolve it
if (briefOrUrl && !options?.brief) {
spinner = ora('Resolving brief...').start();
const resolvedBrief = await this.resolveBriefInput(briefOrUrl);
if (resolvedBrief) {
briefId = resolvedBrief.briefId;
orgId = resolvedBrief.orgId;
spinner.succeed('Brief resolved');
} else {
spinner.fail('Could not resolve brief');
process.exit(1);
}
}
// Validate we have necessary IDs
if (!orgId) {
ui.displayError(
'No organization selected. Run "tm context org" or use --org flag.'
);
process.exit(1);
}
if (!briefId) {
ui.displayError(
'No brief specified. Run "tm context brief", provide a brief ID/URL, or use --brief flag.'
);
process.exit(1);
}
// Confirm export if not auto-confirmed
if (!options?.yes) {
const confirmed = await this.confirmExport(orgId, briefId, context);
if (!confirmed) {
ui.displayWarning('Export cancelled');
this.lastResult = {
success: false,
action: 'cancelled',
message: 'User cancelled export'
};
process.exit(0);
}
}
// Perform export
spinner = ora('Exporting tasks...').start();
// Use integration domain facade
const exportResult = await this.taskMasterCore!.integration.exportTasks({
orgId,
briefId,
tag: options?.tag,
status: options?.status,
excludeSubtasks: options?.excludeSubtasks || false
});
if (exportResult.success) {
spinner.succeed(
`Successfully exported ${exportResult.taskCount} task(s) to brief`
);
// Display summary
console.log(chalk.cyan('\n📤 Export Summary\n'));
console.log(chalk.white(` Organization: ${orgId}`));
console.log(chalk.white(` Brief: ${briefId}`));
console.log(chalk.white(` Tasks exported: ${exportResult.taskCount}`));
if (options?.tag) {
console.log(chalk.gray(` Tag: ${options.tag}`));
}
if (options?.status) {
console.log(chalk.gray(` Status filter: ${options.status}`));
}
if (exportResult.message) {
console.log(chalk.gray(`\n ${exportResult.message}`));
}
} else {
spinner.fail('Export failed');
if (exportResult.error) {
console.error(chalk.red(`\n✗ ${exportResult.error.message}`));
}
}
this.lastResult = {
success: exportResult.success,
action: 'export',
result: exportResult
};
} catch (error: any) {
if (spinner?.isSpinning) spinner.fail('Export failed');
displayError(error);
}
}
/**
* Resolve brief input to get brief and org IDs
*/
private async resolveBriefInput(
briefOrUrl: string
): Promise<{ briefId: string; orgId: string } | null> {
try {
// Extract brief ID from input
const briefId = this.extractBriefId(briefOrUrl);
if (!briefId) {
return null;
}
// Fetch brief to get organization
const brief = await this.authManager.getBrief(briefId);
if (!brief) {
ui.displayError('Brief not found or you do not have access');
return null;
}
return {
briefId: brief.id,
orgId: brief.accountId
};
} catch (error) {
console.error(chalk.red(`Failed to resolve brief: ${error}`));
return null;
}
}
/**
* Extract a brief ID from raw input (ID or URL)
*/
private extractBriefId(input: string): string | null {
const raw = input?.trim() ?? '';
if (!raw) return null;
const parseUrl = (s: string): URL | null => {
try {
return new URL(s);
} catch {}
try {
return new URL(`https://${s}`);
} catch {}
return null;
};
const fromParts = (path: string): string | null => {
const parts = path.split('/').filter(Boolean);
const briefsIdx = parts.lastIndexOf('briefs');
const candidate =
briefsIdx >= 0 && parts.length > briefsIdx + 1
? parts[briefsIdx + 1]
: parts[parts.length - 1];
return candidate?.trim() || null;
};
// Try URL parsing
const url = parseUrl(raw);
if (url) {
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
const candidate = (qId || fromParts(url.pathname)) ?? null;
if (candidate) {
if (this.isLikelyId(candidate) || candidate.length >= 8) {
return candidate;
}
}
}
// Check if it looks like a path
if (raw.includes('/')) {
const candidate = fromParts(raw);
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
return candidate;
}
}
// Return raw if it looks like an ID
return raw;
}
/**
* Check if a string looks like a brief ID
*/
private isLikelyId(value: string): boolean {
const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
const slugRegex = /^[A-Za-z0-9_-]{16,}$/;
return (
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
);
}
/**
* Confirm export with the user
*/
private async confirmExport(
orgId: string,
briefId: string,
context: UserContext | null
): Promise<boolean> {
console.log(chalk.cyan('\n📤 Export Tasks\n'));
// Show org name if available
if (context?.orgName) {
console.log(chalk.white(` Organization: ${context.orgName}`));
console.log(chalk.gray(` ID: ${orgId}`));
} else {
console.log(chalk.white(` Organization ID: ${orgId}`));
}
// Show brief info
if (context?.briefName) {
console.log(chalk.white(`\n Brief: ${context.briefName}`));
console.log(chalk.gray(` ID: ${briefId}`));
} else {
console.log(chalk.white(`\n Brief ID: ${briefId}`));
}
const { confirmed } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmed',
message: 'Do you want to proceed with export?',
default: true
}
]);
return confirmed;
}
/**
* Get the last export result (useful for testing)
*/
public getLastResult(): ExportCommandResult | undefined {
return this.lastResult;
}
/**
* Clean up resources
*/
async cleanup(): Promise<void> {
// No resources to clean up
}
/**
* Register this command on an existing program
*/
static register(program: Command, name?: string): ExportCommand {
const exportCommand = new ExportCommand(name);
program.addCommand(exportCommand);
return exportCommand;
}
}
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/update-tasks.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* Tests for the update-tasks.js module
*/
import { jest } from '@jest/globals';
// Mock the dependencies before importing the module under test
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
readJSON: jest.fn(),
writeJSON: jest.fn(),
log: jest.fn(),
CONFIG: {
model: 'mock-claude-model',
maxTokens: 4000,
temperature: 0.7,
debug: false
},
sanitizePrompt: jest.fn((prompt) => prompt),
truncate: jest.fn((text) => text),
isSilentMode: jest.fn(() => false),
findTaskById: jest.fn(),
getCurrentTag: jest.fn(() => 'master'),
ensureTagMetadata: jest.fn((tagObj) => tagObj),
flattenTasksWithSubtasks: jest.fn((tasks) => tasks),
findProjectRoot: jest.fn(() => '/mock/project/root')
}));
jest.unstable_mockModule(
'../../../../../scripts/modules/ai-services-unified.js',
() => ({
generateTextService: jest.fn().mockResolvedValue({
mainResult: '[]', // mainResult is the text string directly
telemetryData: {}
}),
generateObjectService: jest.fn().mockResolvedValue({
mainResult: {
tasks: [] // generateObject returns structured data
},
telemetryData: {}
})
})
);
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
getStatusWithColor: jest.fn((status) => status),
startLoadingIndicator: jest.fn(),
stopLoadingIndicator: jest.fn(),
displayAiUsageSummary: jest.fn()
}));
jest.unstable_mockModule(
'../../../../../scripts/modules/config-manager.js',
() => ({
getDebugFlag: jest.fn(() => false),
hasCodebaseAnalysis: jest.fn(() => false)
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/task-manager/generate-task-files.js',
() => ({
default: jest.fn().mockResolvedValue()
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/prompt-manager.js',
() => ({
getPromptManager: jest.fn().mockReturnValue({
loadPrompt: jest.fn().mockResolvedValue({
systemPrompt: 'Mocked system prompt',
userPrompt: 'Mocked user prompt'
})
})
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/task-manager/models.js',
() => ({
getModelConfiguration: jest.fn(() => ({
model: 'mock-model',
maxTokens: 4000,
temperature: 0.7
}))
})
);
// Import the mocked modules
const { readJSON, writeJSON, log } = await import(
'../../../../../scripts/modules/utils.js'
);
const { generateObjectService } = await import(
'../../../../../scripts/modules/ai-services-unified.js'
);
// Import the module under test
const { default: updateTasks } = await import(
'../../../../../scripts/modules/task-manager/update-tasks.js'
);
describe('updateTasks', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('should update tasks based on new context', async () => {
// Arrange
const mockTasksPath = '/mock/path/tasks.json';
const mockFromId = 2;
const mockPrompt = 'New project direction';
const mockInitialTasks = {
master: {
tasks: [
{
id: 1,
title: 'Old Task 1',
status: 'done',
details: 'Done details'
},
{
id: 2,
title: 'Old Task 2',
status: 'pending',
details: 'Old details 2'
},
{
id: 3,
title: 'Old Task 3',
status: 'in-progress',
details: 'Old details 3'
}
]
}
};
const mockUpdatedTasks = [
{
id: 2,
title: 'Updated Task 2',
status: 'pending',
details: 'New details 2 based on direction',
description: 'Updated description',
dependencies: [],
priority: 'medium',
testStrategy: 'Unit test the updated functionality',
subtasks: []
},
{
id: 3,
title: 'Updated Task 3',
status: 'pending',
details: 'New details 3 based on direction',
description: 'Updated description',
dependencies: [],
priority: 'medium',
testStrategy: 'Integration test the updated features',
subtasks: []
}
];
const mockApiResponse = {
mainResult: {
tasks: mockUpdatedTasks // generateObject returns structured data
},
telemetryData: {}
};
// Configure mocks - readJSON should return the resolved view with tasks at top level
readJSON.mockReturnValue({
...mockInitialTasks.master,
tag: 'master',
_rawTaggedData: mockInitialTasks
});
generateObjectService.mockResolvedValue(mockApiResponse);
// Act
const result = await updateTasks(
mockTasksPath,
mockFromId,
mockPrompt,
false, // research
{ projectRoot: '/mock/path', tag: 'master' }, // context
'json' // output format
);
// Assert
// 1. Read JSON called
expect(readJSON).toHaveBeenCalledWith(
mockTasksPath,
'/mock/path',
'master'
);
// 2. AI Service called with correct args
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
// 3. Write JSON called with correctly merged tasks
expect(writeJSON).toHaveBeenCalledWith(
mockTasksPath,
expect.objectContaining({
_rawTaggedData: expect.objectContaining({
master: expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 1 }),
expect.objectContaining({ id: 2, title: 'Updated Task 2' }),
expect.objectContaining({ id: 3, title: 'Updated Task 3' })
])
})
})
}),
'/mock/path',
'master'
);
// 4. Check return value
expect(result).toEqual(
expect.objectContaining({
success: true,
updatedTasks: mockUpdatedTasks,
telemetryData: {}
})
);
});
test('should handle no tasks to update', async () => {
// Arrange
const mockTasksPath = '/mock/path/tasks.json';
const mockFromId = 99; // Non-existent ID
const mockPrompt = 'Update non-existent tasks';
const mockInitialTasks = {
master: {
tasks: [
{ id: 1, status: 'done' },
{ id: 2, status: 'done' }
]
}
};
// Configure mocks - readJSON should return the resolved view with tasks at top level
readJSON.mockReturnValue({
...mockInitialTasks.master,
tag: 'master',
_rawTaggedData: mockInitialTasks
});
// Act
const result = await updateTasks(
mockTasksPath,
mockFromId,
mockPrompt,
false,
{ projectRoot: '/mock/path', tag: 'master' },
'json'
);
// Assert
expect(readJSON).toHaveBeenCalledWith(
mockTasksPath,
'/mock/path',
'master'
);
expect(generateObjectService).not.toHaveBeenCalled();
expect(writeJSON).not.toHaveBeenCalled();
expect(log).toHaveBeenCalledWith(
'info',
expect.stringContaining('No tasks to update')
);
// Should return early with no updates
expect(result).toBeUndefined();
});
test('should preserve all tags when updating tasks in tagged context', async () => {
// Arrange - Simple 2-tag structure to test tag corruption fix
const mockTasksPath = '/mock/path/tasks.json';
const mockFromId = 1;
const mockPrompt = 'Update master tag tasks';
const mockTaggedData = {
master: {
tasks: [
{
id: 1,
title: 'Master Task',
status: 'pending',
details: 'Old details'
},
{
id: 2,
title: 'Master Task 2',
status: 'done',
details: 'Done task'
}
],
metadata: {
created: '2024-01-01T00:00:00.000Z',
description: 'Master tag tasks'
}
},
'feature-branch': {
tasks: [
{
id: 1,
title: 'Feature Task',
status: 'pending',
details: 'Feature work'
}
],
metadata: {
created: '2024-01-02T00:00:00.000Z',
description: 'Feature branch tasks'
}
}
};
const mockUpdatedTasks = [
{
id: 1,
title: 'Updated Master Task',
status: 'pending',
details: 'Updated details',
description: 'Updated description',
dependencies: [],
priority: 'medium',
testStrategy: 'Test the updated functionality',
subtasks: []
}
];
// Configure mocks - readJSON returns resolved view for master tag
readJSON.mockReturnValue({
...mockTaggedData.master,
tag: 'master',
_rawTaggedData: mockTaggedData
});
generateObjectService.mockResolvedValue({
mainResult: {
tasks: mockUpdatedTasks
},
telemetryData: { commandName: 'update-tasks', totalCost: 0.05 }
});
// Act
const result = await updateTasks(
mockTasksPath,
mockFromId,
mockPrompt,
false, // research
{ projectRoot: '/mock/project/root', tag: 'master' },
'json'
);
// Assert - CRITICAL: Both tags must be preserved (this would fail before the fix)
expect(writeJSON).toHaveBeenCalledWith(
mockTasksPath,
expect.objectContaining({
_rawTaggedData: expect.objectContaining({
master: expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 1, title: 'Updated Master Task' }),
expect.objectContaining({ id: 2, title: 'Master Task 2' }) // Unchanged done task
])
}),
// CRITICAL: This tag would be missing/corrupted if the bug existed
'feature-branch': expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({ id: 1, title: 'Feature Task' })
]),
metadata: expect.objectContaining({
description: 'Feature branch tasks'
})
})
})
}),
'/mock/project/root',
'master'
);
expect(result.success).toBe(true);
expect(result.updatedTasks).toEqual(mockUpdatedTasks);
});
});
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/git/services/commit-message-generator.test.ts:
--------------------------------------------------------------------------------
```typescript
import { beforeEach, describe, expect, it } from 'vitest';
import { CommitMessageGenerator } from './commit-message-generator.js';
describe('CommitMessageGenerator', () => {
let generator: CommitMessageGenerator;
beforeEach(() => {
generator = new CommitMessageGenerator();
});
describe('generateMessage', () => {
it('should generate basic conventional commit message', () => {
const message = generator.generateMessage({
type: 'feat',
description: 'add user authentication',
changedFiles: ['packages/tm-core/src/auth/auth-manager.ts']
});
expect(message).toContain('feat(core): add user authentication');
});
it('should include scope from changed files', () => {
const message = generator.generateMessage({
type: 'fix',
description: 'resolve CLI argument parsing',
changedFiles: ['packages/cli/src/commands/start.ts']
});
expect(message).toContain('fix(cli): resolve CLI argument parsing');
});
it('should include task metadata in commit body', () => {
const message = generator.generateMessage({
type: 'feat',
description: 'implement feature',
changedFiles: ['packages/tm-core/src/index.ts'],
taskId: '5.3',
phase: 'GREEN'
});
expect(message).toContain('Task: 5.3');
expect(message).toContain('Phase: GREEN');
});
it('should include test results metadata', () => {
const message = generator.generateMessage({
type: 'test',
description: 'add unit tests',
changedFiles: ['packages/tm-core/src/auth/auth.test.ts'],
testsPassing: 42,
testsFailing: 0
});
expect(message).toContain('Tests: 42 passing');
});
it('should include failing test count when present', () => {
const message = generator.generateMessage({
type: 'fix',
description: 'fix test failures',
changedFiles: ['packages/tm-core/src/index.ts'],
testsPassing: 40,
testsFailing: 2
});
expect(message).toContain('Tests: 40 passing, 2 failing');
});
it('should include custom body text', () => {
const message = generator.generateMessage({
type: 'feat',
description: 'add new feature',
changedFiles: ['packages/tm-core/src/index.ts'],
body: 'This is a detailed explanation\nof the changes made.'
});
expect(message).toContain('This is a detailed explanation');
expect(message).toContain('of the changes made.');
});
it('should handle multiple changed files with different scopes', () => {
const message = generator.generateMessage({
type: 'refactor',
description: 'reorganize code structure',
changedFiles: [
'packages/cli/src/index.ts',
'packages/tm-core/src/index.ts'
]
});
// Should use CLI scope (higher priority due to count or priority)
expect(message).toMatch(/refactor\((cli|core)\):/);
});
it('should handle test files and detect test scope', () => {
const message = generator.generateMessage({
type: 'test',
description: 'add integration tests',
changedFiles: ['packages/tm-core/src/workflow/workflow.test.ts']
});
expect(message).toContain('test(test):');
});
it('should handle docs changes', () => {
const message = generator.generateMessage({
type: 'docs',
description: 'update README',
changedFiles: ['README.md', 'docs/guide.md']
});
expect(message).toContain('docs(docs):');
});
it('should omit scope if not detected', () => {
const message = generator.generateMessage({
type: 'chore',
description: 'update dependencies',
changedFiles: []
});
expect(message).toContain('chore(repo): update dependencies');
});
it('should support manual scope override', () => {
const message = generator.generateMessage({
type: 'feat',
description: 'add feature',
changedFiles: ['packages/tm-core/src/index.ts'],
scope: 'api'
});
expect(message).toContain('feat(api): add feature');
});
it('should handle breaking changes indicator', () => {
const message = generator.generateMessage({
type: 'feat',
description: 'change API structure',
changedFiles: ['packages/tm-core/src/index.ts'],
breaking: true
});
expect(message).toContain('feat(core)!: change API structure');
});
it('should format complete message with all metadata', () => {
const message = generator.generateMessage({
type: 'feat',
description: 'implement TDD workflow',
changedFiles: ['packages/tm-core/src/workflow/orchestrator.ts'],
body: 'Implemented complete RED-GREEN-COMMIT cycle with state persistence.',
taskId: '4.1',
phase: 'GREEN',
testsPassing: 74,
testsFailing: 0
});
expect(message).toContain('feat(core): implement TDD workflow');
expect(message).toContain('Implemented complete RED-GREEN-COMMIT cycle');
expect(message).toContain('Task: 4.1');
expect(message).toContain('Phase: GREEN');
expect(message).toContain('Tests: 74 passing');
});
});
describe('validateConventionalCommit', () => {
it('should validate correct conventional commit format', () => {
const message = 'feat(core): add feature\n\nDetails here.';
const result = generator.validateConventionalCommit(message);
expect(result.isValid).toBe(true);
expect(result.errors).toEqual([]);
});
it('should detect missing type', () => {
const message = 'add feature';
const result = generator.validateConventionalCommit(message);
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
expect(result.errors[0]).toContain('Invalid conventional commit format');
});
it('should detect invalid type', () => {
const message = 'invalid(core): add feature';
const result = generator.validateConventionalCommit(message);
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
it('should detect missing description', () => {
const message = 'feat(core):';
const result = generator.validateConventionalCommit(message);
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
expect(result.errors[0]).toContain('Invalid conventional commit format');
});
it('should accept valid types', () => {
const validTypes = [
'feat',
'fix',
'docs',
'style',
'refactor',
'test',
'chore'
];
for (const type of validTypes) {
const message = `${type}(core): do something`;
const result = generator.validateConventionalCommit(message);
expect(result.isValid).toBe(true);
}
});
it('should accept breaking change indicator', () => {
const message = 'feat(core)!: breaking change';
const result = generator.validateConventionalCommit(message);
expect(result.isValid).toBe(true);
});
it('should accept message without scope', () => {
const message = 'fix: resolve issue';
const result = generator.validateConventionalCommit(message);
expect(result.isValid).toBe(true);
});
});
describe('parseCommitMessage', () => {
it('should parse conventional commit message', () => {
const message = 'feat(core): add feature\n\nDetailed explanation.';
const parsed = generator.parseCommitMessage(message);
expect(parsed.type).toBe('feat');
expect(parsed.scope).toBe('core');
expect(parsed.description).toBe('add feature');
expect(parsed.body).toContain('Detailed explanation.');
expect(parsed.breaking).toBe(false);
});
it('should parse breaking change indicator', () => {
const message = 'feat(core)!: breaking change';
const parsed = generator.parseCommitMessage(message);
expect(parsed.type).toBe('feat');
expect(parsed.breaking).toBe(true);
});
it('should parse message without scope', () => {
const message = 'fix: resolve issue';
const parsed = generator.parseCommitMessage(message);
expect(parsed.type).toBe('fix');
expect(parsed.scope).toBeUndefined();
expect(parsed.description).toBe('resolve issue');
});
it('should handle multiline body', () => {
const message = 'feat: add feature\n\nLine 1\nLine 2\nLine 3';
const parsed = generator.parseCommitMessage(message);
expect(parsed.body).toContain('Line 1');
expect(parsed.body).toContain('Line 2');
expect(parsed.body).toContain('Line 3');
});
});
describe('edge cases', () => {
it('should handle empty changed files list', () => {
const message = generator.generateMessage({
type: 'chore',
description: 'general maintenance',
changedFiles: []
});
expect(message).toContain('chore(repo):');
});
it('should handle very long description', () => {
const longDesc = 'a'.repeat(200);
const message = generator.generateMessage({
type: 'feat',
description: longDesc,
changedFiles: ['packages/tm-core/src/index.ts']
});
expect(message).toContain(longDesc);
});
it('should handle special characters in description', () => {
const message = generator.generateMessage({
type: 'fix',
description: 'resolve issue with $special @characters #123',
changedFiles: ['packages/tm-core/src/index.ts']
});
expect(message).toContain('$special @characters #123');
});
it('should handle zero passing tests', () => {
const message = generator.generateMessage({
type: 'test',
description: 'add failing test',
changedFiles: ['test.ts'],
testsPassing: 0,
testsFailing: 1
});
expect(message).toContain('Tests: 0 passing, 1 failing');
});
});
});
```
--------------------------------------------------------------------------------
/tests/manual/progress/parse-prd-analysis.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
/**
* parse-prd-analysis.js
*
* Detailed timing and accuracy analysis for parse-prd progress reporting.
* Tests different task generation complexities using the sample PRD from fixtures.
* Validates real-time characteristics and focuses on progress behavior and performance metrics.
* Uses tests/fixtures/sample-prd.txt for consistent testing across all scenarios.
*/
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
import parsePRD from '../../../scripts/modules/task-manager/parse-prd/index.js';
// Use the same project root as the main test file
const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..');
/**
* Get the path to the sample PRD file
*/
function getSamplePRDPath() {
return path.resolve(PROJECT_ROOT, 'tests', 'fixtures', 'sample-prd.txt');
}
/**
* Detailed Progress Reporter for timing analysis
*/
class DetailedProgressReporter {
constructor() {
this.progressHistory = [];
this.startTime = Date.now();
this.lastProgress = 0;
}
async reportProgress(data) {
const timestamp = Date.now() - this.startTime;
const timeSinceLastProgress =
this.progressHistory.length > 0
? timestamp -
this.progressHistory[this.progressHistory.length - 1].timestamp
: timestamp;
const entry = {
timestamp,
timeSinceLastProgress,
...data
};
this.progressHistory.push(entry);
const percentage = data.total
? Math.round((data.progress / data.total) * 100)
: 0;
console.log(
chalk.blue(`[${timestamp}ms] (+${timeSinceLastProgress}ms)`),
chalk.green(`${percentage}%`),
`(${data.progress}/${data.total})`,
chalk.yellow(data.message)
);
}
getAnalysis() {
if (this.progressHistory.length === 0) return null;
const totalDuration =
this.progressHistory[this.progressHistory.length - 1].timestamp;
const intervals = this.progressHistory
.slice(1)
.map((entry) => entry.timeSinceLastProgress);
const avgInterval =
intervals.length > 0
? intervals.reduce((a, b) => a + b, 0) / intervals.length
: 0;
const minInterval = intervals.length > 0 ? Math.min(...intervals) : 0;
const maxInterval = intervals.length > 0 ? Math.max(...intervals) : 0;
return {
totalReports: this.progressHistory.length,
totalDuration,
avgInterval: Math.round(avgInterval),
minInterval,
maxInterval,
intervals
};
}
printDetailedAnalysis() {
const analysis = this.getAnalysis();
if (!analysis) {
console.log(chalk.red('No progress data to analyze'));
return;
}
console.log(chalk.cyan('\n=== Detailed Progress Analysis ==='));
console.log(`Total Progress Reports: ${analysis.totalReports}`);
console.log(`Total Duration: ${analysis.totalDuration}ms`);
console.log(`Average Interval: ${analysis.avgInterval}ms`);
console.log(`Min Interval: ${analysis.minInterval}ms`);
console.log(`Max Interval: ${analysis.maxInterval}ms`);
console.log(chalk.cyan('\n=== Progress Timeline ==='));
this.progressHistory.forEach((entry, index) => {
const percentage = entry.total
? Math.round((entry.progress / entry.total) * 100)
: 0;
const intervalText =
index > 0 ? ` (+${entry.timeSinceLastProgress}ms)` : '';
console.log(
`${index + 1}. [${entry.timestamp}ms]${intervalText} ${percentage}% - ${entry.message}`
);
});
// Check for real-time characteristics
console.log(chalk.cyan('\n=== Real-time Characteristics ==='));
const hasRealTimeUpdates = analysis.intervals.some(
(interval) => interval < 10000
); // Less than 10s
const hasConsistentUpdates = analysis.intervals.length > 3;
const hasProgressiveUpdates = this.progressHistory.every(
(entry, index) =>
index === 0 ||
entry.progress >= this.progressHistory[index - 1].progress
);
console.log(`✅ Real-time updates: ${hasRealTimeUpdates ? 'YES' : 'NO'}`);
console.log(
`✅ Consistent updates: ${hasConsistentUpdates ? 'YES' : 'NO'}`
);
console.log(
`✅ Progressive updates: ${hasProgressiveUpdates ? 'YES' : 'NO'}`
);
}
}
/**
* Get PRD path for complexity testing
* For complexity testing, we'll use the same sample PRD but request different numbers of tasks
* This provides more realistic testing since the AI will generate different complexity based on task count
*/
function getPRDPathForComplexity(complexity = 'medium') {
// Always use the same sample PRD file - complexity will be controlled by task count
return getSamplePRDPath();
}
/**
* Test streaming with different task generation complexities
* Uses the same sample PRD but requests different numbers of tasks to test complexity scaling
*/
async function testStreamingComplexity() {
console.log(
chalk.cyan(
'🧪 Testing Streaming with Different Task Generation Complexities\n'
)
);
const complexities = ['simple', 'medium', 'complex'];
const results = [];
for (const complexity of complexities) {
console.log(
chalk.yellow(`\n--- Testing ${complexity.toUpperCase()} Complexity ---`)
);
const testPRDPath = getPRDPathForComplexity(complexity);
const testTasksPath = path.join(__dirname, `test-tasks-${complexity}.json`);
// Clean up existing file
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
const progressReporter = new DetailedProgressReporter();
const expectedTasks =
complexity === 'simple' ? 3 : complexity === 'medium' ? 6 : 10;
try {
const startTime = Date.now();
await parsePRD(testPRDPath, testTasksPath, expectedTasks, {
force: true,
append: false,
research: false,
reportProgress: progressReporter.reportProgress.bind(progressReporter),
projectRoot: PROJECT_ROOT
});
const endTime = Date.now();
const duration = endTime - startTime;
console.log(
chalk.green(`✅ ${complexity} complexity completed in ${duration}ms`)
);
progressReporter.printDetailedAnalysis();
results.push({
complexity,
duration,
analysis: progressReporter.getAnalysis()
});
} catch (error) {
console.error(
chalk.red(`❌ ${complexity} complexity failed: ${error.message}`)
);
results.push({
complexity,
error: error.message
});
} finally {
// Clean up (only the tasks file, not the PRD since we're using the fixture)
if (fs.existsSync(testTasksPath)) fs.unlinkSync(testTasksPath);
}
}
// Summary
console.log(chalk.cyan('\n=== Complexity Test Summary ==='));
results.forEach((result) => {
if (result.error) {
console.log(`${result.complexity}: ❌ FAILED - ${result.error}`);
} else {
console.log(
`${result.complexity}: ✅ ${result.duration}ms (${result.analysis.totalReports} reports)`
);
}
});
return results;
}
/**
* Test progress accuracy
*/
async function testProgressAccuracy() {
console.log(chalk.cyan('🧪 Testing Progress Accuracy\n'));
const testPRDPath = getSamplePRDPath();
const testTasksPath = path.join(__dirname, 'test-accuracy-tasks.json');
// Clean up existing file
if (fs.existsSync(testTasksPath)) {
fs.unlinkSync(testTasksPath);
}
const progressReporter = new DetailedProgressReporter();
try {
await parsePRD(testPRDPath, testTasksPath, 8, {
force: true,
append: false,
research: false,
reportProgress: progressReporter.reportProgress.bind(progressReporter),
projectRoot: PROJECT_ROOT
});
console.log(chalk.green('✅ Progress accuracy test completed'));
progressReporter.printDetailedAnalysis();
// Additional accuracy checks
const analysis = progressReporter.getAnalysis();
console.log(chalk.cyan('\n=== Accuracy Metrics ==='));
console.log(
`Progress consistency: ${analysis.intervals.every((i) => i > 0) ? 'PASS' : 'FAIL'}`
);
console.log(
`Reasonable intervals: ${analysis.intervals.every((i) => i < 30000) ? 'PASS' : 'FAIL'}`
);
console.log(
`Expected report count: ${analysis.totalReports >= 8 ? 'PASS' : 'FAIL'}`
);
} catch (error) {
console.error(
chalk.red(`❌ Progress accuracy test failed: ${error.message}`)
);
} finally {
// Clean up (only the tasks file, not the PRD since we're using the fixture)
if (fs.existsSync(testTasksPath)) fs.unlinkSync(testTasksPath);
}
}
/**
* Main test runner
*/
async function main() {
const args = process.argv.slice(2);
const testType = args[0] || 'accuracy';
console.log(chalk.bold.cyan('🚀 Task Master Detailed Progress Tests\n'));
console.log(chalk.blue(`Test type: ${testType}\n`));
try {
switch (testType.toLowerCase()) {
case 'accuracy':
await testProgressAccuracy();
break;
case 'complexity':
await testStreamingComplexity();
break;
case 'all':
console.log(chalk.yellow('Running all detailed tests...\n'));
await testProgressAccuracy();
console.log('\n' + '='.repeat(60) + '\n');
await testStreamingComplexity();
break;
default:
console.log(chalk.red(`Unknown test type: ${testType}`));
console.log(
chalk.yellow('Available options: accuracy, complexity, all')
);
process.exit(1);
}
console.log(chalk.green('\n🎉 Detailed tests completed successfully!'));
} catch (error) {
console.error(chalk.red(`\n❌ Test failed: ${error.message}`));
console.error(chalk.red(error.stack));
process.exit(1);
}
}
// Run if called directly
if (import.meta.url === `file://${process.argv[1]}`) {
// Top-level await is available in ESM; keep compatibility with Node ≥14
await main();
}
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/utils/path-normalizer.spec.ts:
--------------------------------------------------------------------------------
```typescript
import { describe, expect, it } from 'vitest';
import {
denormalizeProjectPath,
isValidNormalizedPath,
normalizeProjectPath
} from './path-normalizer.js';
describe('Path Normalizer (base64url encoding)', () => {
describe('normalizeProjectPath', () => {
it('should encode Unix paths to base64url', () => {
const input = '/Users/test/projects/myapp';
const normalized = normalizeProjectPath(input);
// Should be valid base64url (only A-Z, a-z, 0-9, -, _)
expect(/^[A-Za-z0-9_-]+$/.test(normalized)).toBe(true);
// Should not contain slashes
expect(normalized).not.toContain('/');
expect(normalized).not.toContain('\\');
});
it('should encode Windows paths to base64url', () => {
const input = 'C:\\Users\\test\\projects\\myapp';
const normalized = normalizeProjectPath(input);
// Should be valid base64url
expect(/^[A-Za-z0-9_-]+$/.test(normalized)).toBe(true);
expect(normalized).not.toContain('/');
expect(normalized).not.toContain('\\');
});
it('should encode paths with hyphens (preserving them for round-trip)', () => {
const input = '/projects/my-app';
const normalized = normalizeProjectPath(input);
// Should be valid base64url
expect(/^[A-Za-z0-9_-]+$/.test(normalized)).toBe(true);
// Hyphens in base64url are from encoding, not original path
expect(isValidNormalizedPath(normalized)).toBe(true);
});
it('should encode paths with special characters', () => {
const input = '/projects/myapp (v2)';
const normalized = normalizeProjectPath(input);
// Should be valid base64url
expect(/^[A-Za-z0-9_-]+$/.test(normalized)).toBe(true);
});
it('should encode relative paths', () => {
const input = './projects/app';
const normalized = normalizeProjectPath(input);
// Should be valid base64url
expect(/^[A-Za-z0-9_-]+$/.test(normalized)).toBe(true);
});
it('should handle empty string', () => {
const input = '';
const expected = '';
expect(normalizeProjectPath(input)).toBe(expected);
});
it('should encode single directory', () => {
const input = 'project';
const normalized = normalizeProjectPath(input);
// Should be valid base64url
expect(/^[A-Za-z0-9_-]+$/.test(normalized)).toBe(true);
});
it('should encode paths with multiple consecutive slashes', () => {
const input = '/Users//test///project';
const normalized = normalizeProjectPath(input);
// Should be valid base64url
expect(/^[A-Za-z0-9_-]+$/.test(normalized)).toBe(true);
});
});
describe('denormalizeProjectPath', () => {
it('should decode base64url back to original path', () => {
const original = '/Users/test/projects/myapp';
const normalized = normalizeProjectPath(original);
const denormalized = denormalizeProjectPath(normalized);
expect(denormalized).toBe(original);
});
it('should decode base64url for Windows paths', () => {
const original = 'C:\\Users\\test\\project';
const normalized = normalizeProjectPath(original);
const denormalized = denormalizeProjectPath(normalized);
expect(denormalized).toBe(original);
});
it('should handle empty string', () => {
const input = '';
const expected = '';
expect(denormalizeProjectPath(input)).toBe(expected);
});
it('should preserve hyphens in directory names (no longer a limitation!)', () => {
const original = '/projects/my-app';
const normalized = normalizeProjectPath(original);
const denormalized = denormalizeProjectPath(normalized);
// With base64url, hyphens are preserved correctly
expect(denormalized).toBe(original);
});
it('should handle invalid base64url gracefully', () => {
// Invalid base64url - should return the input as fallback
const invalid = 'not@valid#base64url';
const result = denormalizeProjectPath(invalid);
// Should return input unchanged for backward compatibility
expect(result).toBe(invalid);
});
});
describe('isValidNormalizedPath', () => {
it('should return true for valid base64url strings', () => {
// Valid base64url characters: A-Z, a-z, 0-9, -, _
expect(isValidNormalizedPath('VXNlcnMtdGVzdC1wcm9qZWN0')).toBe(true);
expect(isValidNormalizedPath('abc123_-ABC')).toBe(true);
});
it('should return true for base64url with hyphens and underscores', () => {
expect(isValidNormalizedPath('test-path_encoded')).toBe(true);
});
it('should return false for paths with slashes', () => {
expect(isValidNormalizedPath('Users/test/project')).toBe(false);
});
it('should return false for paths with backslashes', () => {
expect(isValidNormalizedPath('Users\\test\\project')).toBe(false);
});
it('should return true for empty string', () => {
expect(isValidNormalizedPath('')).toBe(true);
});
it('should return false for strings with special characters not in base64url', () => {
// Base64url only allows: A-Z, a-z, 0-9, -, _
expect(isValidNormalizedPath('my-app (v2)')).toBe(false); // parentheses and spaces not allowed
expect(isValidNormalizedPath('test@example')).toBe(false); // @ not allowed
expect(isValidNormalizedPath('test+value')).toBe(false); // + not allowed
});
it('should validate normalized paths correctly', () => {
const path = '/Users/test/my-app';
const normalized = normalizeProjectPath(path);
expect(isValidNormalizedPath(normalized)).toBe(true);
});
});
describe('Round-trip conversion', () => {
it('should perfectly preserve ALL Unix paths (including those with hyphens)', () => {
const originalPaths = [
'/Users/test/projects/myapp',
'/root/deep/nested/path',
'./relative/path',
'/projects/my-app', // Now works correctly!
'/path/with-multiple-hyphens/in-names'
];
for (const original of originalPaths) {
const normalized = normalizeProjectPath(original);
const denormalized = denormalizeProjectPath(normalized);
// Perfect round-trip with base64url encoding
expect(denormalized).toBe(original);
}
});
it('should perfectly preserve Windows paths (including drive letters)', () => {
const originalPaths = [
'C:\\Users\\test\\project',
'D:\\Projects\\my-app',
'E:\\path\\with-hyphens\\test'
];
for (const original of originalPaths) {
const normalized = normalizeProjectPath(original);
const denormalized = denormalizeProjectPath(normalized);
// Perfect round-trip - drive letters and colons preserved
expect(denormalized).toBe(original);
}
});
it('should preserve paths with special characters', () => {
const originalPaths = [
'/projects/my app (v2)',
'/path/with spaces/test',
'/path/with-dashes-and_underscores',
'/path/with.dots.and-dashes'
];
for (const original of originalPaths) {
const normalized = normalizeProjectPath(original);
const denormalized = denormalizeProjectPath(normalized);
// Perfect round-trip for all special characters
expect(denormalized).toBe(original);
}
});
it('should handle mixed slashes and preserve exact path structure', () => {
const original = '/Users/test\\mixed/path';
const normalized = normalizeProjectPath(original);
const denormalized = denormalizeProjectPath(normalized);
// Exact preservation of mixed slashes
expect(denormalized).toBe(original);
});
it('should preserve multiple consecutive slashes', () => {
const original = '/Users//test///project';
const normalized = normalizeProjectPath(original);
const denormalized = denormalizeProjectPath(normalized);
// Exact preservation of all slashes
expect(denormalized).toBe(original);
});
});
describe('Cross-platform consistency', () => {
it('should produce filesystem-safe normalized output for all platforms', () => {
const unixPath = '/Users/test/project';
const windowsPath = 'C:\\Users\\test\\project';
const normalizedUnix = normalizeProjectPath(unixPath);
const normalizedWindows = normalizeProjectPath(windowsPath);
// Both should be valid base64url (no slashes or backslashes)
expect(normalizedUnix).not.toContain('/');
expect(normalizedUnix).not.toContain('\\');
expect(normalizedWindows).not.toContain('/');
expect(normalizedWindows).not.toContain('\\');
// Both should be valid base64url format
expect(isValidNormalizedPath(normalizedUnix)).toBe(true);
expect(isValidNormalizedPath(normalizedWindows)).toBe(true);
});
it('should produce different normalized outputs for different paths', () => {
// Unix and Windows paths are different, so should produce different encoded values
const unixPath = '/Users/test/project';
const windowsPath = 'C:\\Users\\test\\project';
const normalizedUnix = normalizeProjectPath(unixPath);
const normalizedWindows = normalizeProjectPath(windowsPath);
// Different inputs should produce different outputs
expect(normalizedUnix).not.toBe(normalizedWindows);
// But both should denormalize back to their originals
expect(denormalizeProjectPath(normalizedUnix)).toBe(unixPath);
expect(denormalizeProjectPath(normalizedWindows)).toBe(windowsPath);
});
it('should handle Unicode characters in paths', () => {
const unicodePaths = [
'/Users/测试/project',
'/Users/test/プロジェクト',
'/Users/тест/project'
];
for (const original of unicodePaths) {
const normalized = normalizeProjectPath(original);
const denormalized = denormalizeProjectPath(normalized);
// Perfect round-trip for Unicode
expect(denormalized).toBe(original);
expect(isValidNormalizedPath(normalized)).toBe(true);
}
});
});
});
```