#
tokens: 48997/50000 10/975 files (page 30/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 30 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .changeset
│   ├── config.json
│   └── README.md
├── .claude
│   ├── commands
│   │   └── dedupe.md
│   └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│   └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│   ├── mcp.json
│   └── rules
│       ├── ai_providers.mdc
│       ├── ai_services.mdc
│       ├── architecture.mdc
│       ├── changeset.mdc
│       ├── commands.mdc
│       ├── context_gathering.mdc
│       ├── cursor_rules.mdc
│       ├── dependencies.mdc
│       ├── dev_workflow.mdc
│       ├── git_workflow.mdc
│       ├── glossary.mdc
│       ├── mcp.mdc
│       ├── new_features.mdc
│       ├── self_improve.mdc
│       ├── tags.mdc
│       ├── taskmaster.mdc
│       ├── tasks.mdc
│       ├── telemetry.mdc
│       ├── test_workflow.mdc
│       ├── tests.mdc
│       ├── ui.mdc
│       └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   ├── enhancements---feature-requests.md
│   │   └── feedback.md
│   ├── PULL_REQUEST_TEMPLATE
│   │   ├── bugfix.md
│   │   ├── config.yml
│   │   ├── feature.md
│   │   └── integration.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── scripts
│   │   ├── auto-close-duplicates.mjs
│   │   ├── backfill-duplicate-comments.mjs
│   │   ├── check-pre-release-mode.mjs
│   │   ├── parse-metrics.mjs
│   │   ├── release.mjs
│   │   ├── tag-extension.mjs
│   │   ├── utils.mjs
│   │   └── validate-changesets.mjs
│   └── workflows
│       ├── auto-close-duplicates.yml
│       ├── backfill-duplicate-comments.yml
│       ├── ci.yml
│       ├── claude-dedupe-issues.yml
│       ├── claude-docs-trigger.yml
│       ├── claude-docs-updater.yml
│       ├── claude-issue-triage.yml
│       ├── claude.yml
│       ├── extension-ci.yml
│       ├── extension-release.yml
│       ├── log-issue-events.yml
│       ├── pre-release.yml
│       ├── release-check.yml
│       ├── release.yml
│       ├── update-models-md.yml
│       └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│   ├── hooks
│   │   ├── tm-code-change-task-tracker.kiro.hook
│   │   ├── tm-complexity-analyzer.kiro.hook
│   │   ├── tm-daily-standup-assistant.kiro.hook
│   │   ├── tm-git-commit-task-linker.kiro.hook
│   │   ├── tm-pr-readiness-checker.kiro.hook
│   │   ├── tm-task-dependency-auto-progression.kiro.hook
│   │   └── tm-test-success-task-completer.kiro.hook
│   ├── settings
│   │   └── mcp.json
│   └── steering
│       ├── dev_workflow.md
│       ├── kiro_rules.md
│       ├── self_improve.md
│       ├── taskmaster_hooks_workflow.md
│       └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│   ├── CLAUDE.md
│   ├── config.json
│   ├── docs
│   │   ├── autonomous-tdd-git-workflow.md
│   │   ├── MIGRATION-ROADMAP.md
│   │   ├── prd-tm-start.txt
│   │   ├── prd.txt
│   │   ├── README.md
│   │   ├── research
│   │   │   ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│   │   │   ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│   │   │   ├── 2025-06-14_test-save-functionality.md
│   │   │   ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│   │   │   └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│   │   ├── task-template-importing-prd.txt
│   │   ├── tdd-workflow-phase-0-spike.md
│   │   ├── tdd-workflow-phase-1-core-rails.md
│   │   ├── tdd-workflow-phase-1-orchestrator.md
│   │   ├── tdd-workflow-phase-2-pr-resumability.md
│   │   ├── tdd-workflow-phase-3-extensibility-guardrails.md
│   │   ├── test-prd.txt
│   │   └── tm-core-phase-1.txt
│   ├── reports
│   │   ├── task-complexity-report_autonomous-tdd-git-workflow.json
│   │   ├── task-complexity-report_cc-kiro-hooks.json
│   │   ├── task-complexity-report_tdd-phase-1-core-rails.json
│   │   ├── task-complexity-report_tdd-workflow-phase-0.json
│   │   ├── task-complexity-report_test-prd-tag.json
│   │   ├── task-complexity-report_tm-core-phase-1.json
│   │   ├── task-complexity-report.json
│   │   └── tm-core-complexity.json
│   ├── state.json
│   ├── tasks
│   │   ├── task_001_tm-start.txt
│   │   ├── task_002_tm-start.txt
│   │   ├── task_003_tm-start.txt
│   │   ├── task_004_tm-start.txt
│   │   ├── task_007_tm-start.txt
│   │   └── tasks.json
│   └── templates
│       ├── example_prd_rpg.md
│       └── example_prd.md
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── apps
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── command-registry.ts
│   │   │   ├── commands
│   │   │   │   ├── auth.command.ts
│   │   │   │   ├── autopilot
│   │   │   │   │   ├── abort.command.ts
│   │   │   │   │   ├── commit.command.ts
│   │   │   │   │   ├── complete.command.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next.command.ts
│   │   │   │   │   ├── resume.command.ts
│   │   │   │   │   ├── shared.ts
│   │   │   │   │   ├── start.command.ts
│   │   │   │   │   └── status.command.ts
│   │   │   │   ├── briefs.command.ts
│   │   │   │   ├── context.command.ts
│   │   │   │   ├── export.command.ts
│   │   │   │   ├── list.command.ts
│   │   │   │   ├── models
│   │   │   │   │   ├── custom-providers.ts
│   │   │   │   │   ├── fetchers.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── prompts.ts
│   │   │   │   │   ├── setup.ts
│   │   │   │   │   └── types.ts
│   │   │   │   ├── next.command.ts
│   │   │   │   ├── set-status.command.ts
│   │   │   │   ├── show.command.ts
│   │   │   │   ├── start.command.ts
│   │   │   │   └── tags.command.ts
│   │   │   ├── index.ts
│   │   │   ├── lib
│   │   │   │   └── model-management.ts
│   │   │   ├── types
│   │   │   │   └── tag-management.d.ts
│   │   │   ├── ui
│   │   │   │   ├── components
│   │   │   │   │   ├── cardBox.component.ts
│   │   │   │   │   ├── dashboard.component.ts
│   │   │   │   │   ├── header.component.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next-task.component.ts
│   │   │   │   │   ├── suggested-steps.component.ts
│   │   │   │   │   └── task-detail.component.ts
│   │   │   │   ├── display
│   │   │   │   │   ├── messages.ts
│   │   │   │   │   └── tables.ts
│   │   │   │   ├── formatters
│   │   │   │   │   ├── complexity-formatters.ts
│   │   │   │   │   ├── dependency-formatters.ts
│   │   │   │   │   ├── priority-formatters.ts
│   │   │   │   │   ├── status-formatters.spec.ts
│   │   │   │   │   └── status-formatters.ts
│   │   │   │   ├── index.ts
│   │   │   │   └── layout
│   │   │   │       ├── helpers.spec.ts
│   │   │   │       └── helpers.ts
│   │   │   └── utils
│   │   │       ├── auth-helpers.ts
│   │   │       ├── auto-update.ts
│   │   │       ├── brief-selection.ts
│   │   │       ├── display-helpers.ts
│   │   │       ├── error-handler.ts
│   │   │       ├── index.ts
│   │   │       ├── project-root.ts
│   │   │       ├── task-status.ts
│   │   │       ├── ui.spec.ts
│   │   │       └── ui.ts
│   │   ├── tests
│   │   │   ├── integration
│   │   │   │   └── commands
│   │   │   │       └── autopilot
│   │   │   │           └── workflow.test.ts
│   │   │   └── unit
│   │   │       ├── commands
│   │   │       │   ├── autopilot
│   │   │       │   │   └── shared.test.ts
│   │   │       │   ├── list.command.spec.ts
│   │   │       │   └── show.command.spec.ts
│   │   │       └── ui
│   │   │           └── dashboard.component.spec.ts
│   │   ├── tsconfig.json
│   │   └── vitest.config.ts
│   ├── docs
│   │   ├── archive
│   │   │   ├── ai-client-utils-example.mdx
│   │   │   ├── ai-development-workflow.mdx
│   │   │   ├── command-reference.mdx
│   │   │   ├── configuration.mdx
│   │   │   ├── cursor-setup.mdx
│   │   │   ├── examples.mdx
│   │   │   └── Installation.mdx
│   │   ├── best-practices
│   │   │   ├── advanced-tasks.mdx
│   │   │   ├── configuration-advanced.mdx
│   │   │   └── index.mdx
│   │   ├── capabilities
│   │   │   ├── cli-root-commands.mdx
│   │   │   ├── index.mdx
│   │   │   ├── mcp.mdx
│   │   │   ├── rpg-method.mdx
│   │   │   └── task-structure.mdx
│   │   ├── CHANGELOG.md
│   │   ├── command-reference.mdx
│   │   ├── configuration.mdx
│   │   ├── docs.json
│   │   ├── favicon.svg
│   │   ├── getting-started
│   │   │   ├── api-keys.mdx
│   │   │   ├── contribute.mdx
│   │   │   ├── faq.mdx
│   │   │   └── quick-start
│   │   │       ├── configuration-quick.mdx
│   │   │       ├── execute-quick.mdx
│   │   │       ├── installation.mdx
│   │   │       ├── moving-forward.mdx
│   │   │       ├── prd-quick.mdx
│   │   │       ├── quick-start.mdx
│   │   │       ├── requirements.mdx
│   │   │       ├── rules-quick.mdx
│   │   │       └── tasks-quick.mdx
│   │   ├── introduction.mdx
│   │   ├── licensing.md
│   │   ├── logo
│   │   │   ├── dark.svg
│   │   │   ├── light.svg
│   │   │   └── task-master-logo.png
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── style.css
│   │   ├── tdd-workflow
│   │   │   ├── ai-agent-integration.mdx
│   │   │   └── quickstart.mdx
│   │   ├── vercel.json
│   │   └── whats-new.mdx
│   ├── extension
│   │   ├── .vscodeignore
│   │   ├── assets
│   │   │   ├── banner.png
│   │   │   ├── icon-dark.svg
│   │   │   ├── icon-light.svg
│   │   │   ├── icon.png
│   │   │   ├── screenshots
│   │   │   │   ├── kanban-board.png
│   │   │   │   └── task-details.png
│   │   │   └── sidebar-icon.svg
│   │   ├── CHANGELOG.md
│   │   ├── components.json
│   │   ├── docs
│   │   │   ├── extension-CI-setup.md
│   │   │   └── extension-development-guide.md
│   │   ├── esbuild.js
│   │   ├── LICENSE
│   │   ├── package.json
│   │   ├── package.mjs
│   │   ├── package.publish.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── components
│   │   │   │   ├── ConfigView.tsx
│   │   │   │   ├── constants.ts
│   │   │   │   ├── TaskDetails
│   │   │   │   │   ├── AIActionsSection.tsx
│   │   │   │   │   ├── DetailsSection.tsx
│   │   │   │   │   ├── PriorityBadge.tsx
│   │   │   │   │   ├── SubtasksSection.tsx
│   │   │   │   │   ├── TaskMetadataSidebar.tsx
│   │   │   │   │   └── useTaskDetails.ts
│   │   │   │   ├── TaskDetailsView.tsx
│   │   │   │   ├── TaskMasterLogo.tsx
│   │   │   │   └── ui
│   │   │   │       ├── badge.tsx
│   │   │   │       ├── breadcrumb.tsx
│   │   │   │       ├── button.tsx
│   │   │   │       ├── card.tsx
│   │   │   │       ├── collapsible.tsx
│   │   │   │       ├── CollapsibleSection.tsx
│   │   │   │       ├── dropdown-menu.tsx
│   │   │   │       ├── label.tsx
│   │   │   │       ├── scroll-area.tsx
│   │   │   │       ├── separator.tsx
│   │   │   │       ├── shadcn-io
│   │   │   │       │   └── kanban
│   │   │   │       │       └── index.tsx
│   │   │   │       └── textarea.tsx
│   │   │   ├── extension.ts
│   │   │   ├── index.ts
│   │   │   ├── lib
│   │   │   │   └── utils.ts
│   │   │   ├── services
│   │   │   │   ├── config-service.ts
│   │   │   │   ├── error-handler.ts
│   │   │   │   ├── notification-preferences.ts
│   │   │   │   ├── polling-service.ts
│   │   │   │   ├── polling-strategies.ts
│   │   │   │   ├── sidebar-webview-manager.ts
│   │   │   │   ├── task-repository.ts
│   │   │   │   ├── terminal-manager.ts
│   │   │   │   └── webview-manager.ts
│   │   │   ├── test
│   │   │   │   └── extension.test.ts
│   │   │   ├── utils
│   │   │   │   ├── configManager.ts
│   │   │   │   ├── connectionManager.ts
│   │   │   │   ├── errorHandler.ts
│   │   │   │   ├── event-emitter.ts
│   │   │   │   ├── logger.ts
│   │   │   │   ├── mcpClient.ts
│   │   │   │   ├── notificationPreferences.ts
│   │   │   │   └── task-master-api
│   │   │   │       ├── cache
│   │   │   │       │   └── cache-manager.ts
│   │   │   │       ├── index.ts
│   │   │   │       ├── mcp-client.ts
│   │   │   │       ├── transformers
│   │   │   │       │   └── task-transformer.ts
│   │   │   │       └── types
│   │   │   │           └── index.ts
│   │   │   └── webview
│   │   │       ├── App.tsx
│   │   │       ├── components
│   │   │       │   ├── AppContent.tsx
│   │   │       │   ├── EmptyState.tsx
│   │   │       │   ├── ErrorBoundary.tsx
│   │   │       │   ├── PollingStatus.tsx
│   │   │       │   ├── PriorityBadge.tsx
│   │   │       │   ├── SidebarView.tsx
│   │   │       │   ├── TagDropdown.tsx
│   │   │       │   ├── TaskCard.tsx
│   │   │       │   ├── TaskEditModal.tsx
│   │   │       │   ├── TaskMasterKanban.tsx
│   │   │       │   ├── ToastContainer.tsx
│   │   │       │   └── ToastNotification.tsx
│   │   │       ├── constants
│   │   │       │   └── index.ts
│   │   │       ├── contexts
│   │   │       │   └── VSCodeContext.tsx
│   │   │       ├── hooks
│   │   │       │   ├── useTaskQueries.ts
│   │   │       │   ├── useVSCodeMessages.ts
│   │   │       │   └── useWebviewHeight.ts
│   │   │       ├── index.css
│   │   │       ├── index.tsx
│   │   │       ├── providers
│   │   │       │   └── QueryProvider.tsx
│   │   │       ├── reducers
│   │   │       │   └── appReducer.ts
│   │   │       ├── sidebar.tsx
│   │   │       ├── types
│   │   │       │   └── index.ts
│   │   │       └── utils
│   │   │           ├── logger.ts
│   │   │           └── toast.ts
│   │   └── tsconfig.json
│   └── mcp
│       ├── CHANGELOG.md
│       ├── package.json
│       ├── src
│       │   ├── index.ts
│       │   ├── shared
│       │   │   ├── types.ts
│       │   │   └── utils.ts
│       │   └── tools
│       │       ├── autopilot
│       │       │   ├── abort.tool.ts
│       │       │   ├── commit.tool.ts
│       │       │   ├── complete.tool.ts
│       │       │   ├── finalize.tool.ts
│       │       │   ├── index.ts
│       │       │   ├── next.tool.ts
│       │       │   ├── resume.tool.ts
│       │       │   ├── start.tool.ts
│       │       │   └── status.tool.ts
│       │       ├── README-ZOD-V3.md
│       │       └── tasks
│       │           ├── get-task.tool.ts
│       │           ├── get-tasks.tool.ts
│       │           └── index.ts
│       ├── tsconfig.json
│       └── vitest.config.ts
├── assets
│   ├── .windsurfrules
│   ├── AGENTS.md
│   ├── claude
│   │   └── TM_COMMANDS_GUIDE.md
│   ├── config.json
│   ├── env.example
│   ├── example_prd_rpg.txt
│   ├── example_prd.txt
│   ├── GEMINI.md
│   ├── gitignore
│   ├── kiro-hooks
│   │   ├── tm-code-change-task-tracker.kiro.hook
│   │   ├── tm-complexity-analyzer.kiro.hook
│   │   ├── tm-daily-standup-assistant.kiro.hook
│   │   ├── tm-git-commit-task-linker.kiro.hook
│   │   ├── tm-pr-readiness-checker.kiro.hook
│   │   ├── tm-task-dependency-auto-progression.kiro.hook
│   │   └── tm-test-success-task-completer.kiro.hook
│   ├── roocode
│   │   ├── .roo
│   │   │   ├── rules-architect
│   │   │   │   └── architect-rules
│   │   │   ├── rules-ask
│   │   │   │   └── ask-rules
│   │   │   ├── rules-code
│   │   │   │   └── code-rules
│   │   │   ├── rules-debug
│   │   │   │   └── debug-rules
│   │   │   ├── rules-orchestrator
│   │   │   │   └── orchestrator-rules
│   │   │   └── rules-test
│   │   │       └── test-rules
│   │   └── .roomodes
│   ├── rules
│   │   ├── cursor_rules.mdc
│   │   ├── dev_workflow.mdc
│   │   ├── self_improve.mdc
│   │   ├── taskmaster_hooks_workflow.mdc
│   │   └── taskmaster.mdc
│   └── scripts_README.md
├── bin
│   └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│   ├── chats
│   │   ├── add-task-dependencies-1.md
│   │   └── max-min-tokens.txt.md
│   ├── fastmcp-core.txt
│   ├── fastmcp-docs.txt
│   ├── MCP_INTEGRATION.md
│   ├── mcp-js-sdk-docs.txt
│   ├── mcp-protocol-repo.txt
│   ├── mcp-protocol-schema-03262025.json
│   └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│   ├── claude-code-integration.md
│   ├── CLI-COMMANDER-PATTERN.md
│   ├── command-reference.md
│   ├── configuration.md
│   ├── contributor-docs
│   │   ├── testing-roo-integration.md
│   │   └── worktree-setup.md
│   ├── cross-tag-task-movement.md
│   ├── examples
│   │   ├── claude-code-usage.md
│   │   └── codex-cli-usage.md
│   ├── examples.md
│   ├── licensing.md
│   ├── mcp-provider-guide.md
│   ├── mcp-provider.md
│   ├── migration-guide.md
│   ├── models.md
│   ├── providers
│   │   ├── codex-cli.md
│   │   └── gemini-cli.md
│   ├── README.md
│   ├── scripts
│   │   └── models-json-to-markdown.js
│   ├── task-structure.md
│   └── tutorial.md
├── images
│   ├── hamster-hiring.png
│   └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│   ├── server.js
│   └── src
│       ├── core
│       │   ├── __tests__
│       │   │   └── context-manager.test.js
│       │   ├── context-manager.js
│       │   ├── direct-functions
│       │   │   ├── add-dependency.js
│       │   │   ├── add-subtask.js
│       │   │   ├── add-tag.js
│       │   │   ├── add-task.js
│       │   │   ├── analyze-task-complexity.js
│       │   │   ├── cache-stats.js
│       │   │   ├── clear-subtasks.js
│       │   │   ├── complexity-report.js
│       │   │   ├── copy-tag.js
│       │   │   ├── create-tag-from-branch.js
│       │   │   ├── delete-tag.js
│       │   │   ├── expand-all-tasks.js
│       │   │   ├── expand-task.js
│       │   │   ├── fix-dependencies.js
│       │   │   ├── generate-task-files.js
│       │   │   ├── initialize-project.js
│       │   │   ├── list-tags.js
│       │   │   ├── models.js
│       │   │   ├── move-task-cross-tag.js
│       │   │   ├── move-task.js
│       │   │   ├── next-task.js
│       │   │   ├── parse-prd.js
│       │   │   ├── remove-dependency.js
│       │   │   ├── remove-subtask.js
│       │   │   ├── remove-task.js
│       │   │   ├── rename-tag.js
│       │   │   ├── research.js
│       │   │   ├── response-language.js
│       │   │   ├── rules.js
│       │   │   ├── scope-down.js
│       │   │   ├── scope-up.js
│       │   │   ├── set-task-status.js
│       │   │   ├── update-subtask-by-id.js
│       │   │   ├── update-task-by-id.js
│       │   │   ├── update-tasks.js
│       │   │   ├── use-tag.js
│       │   │   └── validate-dependencies.js
│       │   ├── task-master-core.js
│       │   └── utils
│       │       ├── env-utils.js
│       │       └── path-utils.js
│       ├── custom-sdk
│       │   ├── errors.js
│       │   ├── index.js
│       │   ├── json-extractor.js
│       │   ├── language-model.js
│       │   ├── message-converter.js
│       │   └── schema-converter.js
│       ├── index.js
│       ├── logger.js
│       ├── providers
│       │   └── mcp-provider.js
│       └── tools
│           ├── add-dependency.js
│           ├── add-subtask.js
│           ├── add-tag.js
│           ├── add-task.js
│           ├── analyze.js
│           ├── clear-subtasks.js
│           ├── complexity-report.js
│           ├── copy-tag.js
│           ├── delete-tag.js
│           ├── expand-all.js
│           ├── expand-task.js
│           ├── fix-dependencies.js
│           ├── generate.js
│           ├── get-operation-status.js
│           ├── index.js
│           ├── initialize-project.js
│           ├── list-tags.js
│           ├── models.js
│           ├── move-task.js
│           ├── next-task.js
│           ├── parse-prd.js
│           ├── README-ZOD-V3.md
│           ├── remove-dependency.js
│           ├── remove-subtask.js
│           ├── remove-task.js
│           ├── rename-tag.js
│           ├── research.js
│           ├── response-language.js
│           ├── rules.js
│           ├── scope-down.js
│           ├── scope-up.js
│           ├── set-task-status.js
│           ├── tool-registry.js
│           ├── update-subtask.js
│           ├── update-task.js
│           ├── update.js
│           ├── use-tag.js
│           ├── utils.js
│           └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│   ├── ai-sdk-provider-grok-cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── errors.test.ts
│   │   │   ├── errors.ts
│   │   │   ├── grok-cli-language-model.ts
│   │   │   ├── grok-cli-provider.test.ts
│   │   │   ├── grok-cli-provider.ts
│   │   │   ├── index.ts
│   │   │   ├── json-extractor.test.ts
│   │   │   ├── json-extractor.ts
│   │   │   ├── message-converter.test.ts
│   │   │   ├── message-converter.ts
│   │   │   └── types.ts
│   │   └── tsconfig.json
│   ├── build-config
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   └── tsdown.base.ts
│   │   └── tsconfig.json
│   ├── claude-code-plugin
│   │   ├── .claude-plugin
│   │   │   └── plugin.json
│   │   ├── .gitignore
│   │   ├── agents
│   │   │   ├── task-checker.md
│   │   │   ├── task-executor.md
│   │   │   └── task-orchestrator.md
│   │   ├── CHANGELOG.md
│   │   ├── commands
│   │   │   ├── add-dependency.md
│   │   │   ├── add-subtask.md
│   │   │   ├── add-task.md
│   │   │   ├── analyze-complexity.md
│   │   │   ├── analyze-project.md
│   │   │   ├── auto-implement-tasks.md
│   │   │   ├── command-pipeline.md
│   │   │   ├── complexity-report.md
│   │   │   ├── convert-task-to-subtask.md
│   │   │   ├── expand-all-tasks.md
│   │   │   ├── expand-task.md
│   │   │   ├── fix-dependencies.md
│   │   │   ├── generate-tasks.md
│   │   │   ├── help.md
│   │   │   ├── init-project-quick.md
│   │   │   ├── init-project.md
│   │   │   ├── install-taskmaster.md
│   │   │   ├── learn.md
│   │   │   ├── list-tasks-by-status.md
│   │   │   ├── list-tasks-with-subtasks.md
│   │   │   ├── list-tasks.md
│   │   │   ├── next-task.md
│   │   │   ├── parse-prd-with-research.md
│   │   │   ├── parse-prd.md
│   │   │   ├── project-status.md
│   │   │   ├── quick-install-taskmaster.md
│   │   │   ├── remove-all-subtasks.md
│   │   │   ├── remove-dependency.md
│   │   │   ├── remove-subtask.md
│   │   │   ├── remove-subtasks.md
│   │   │   ├── remove-task.md
│   │   │   ├── setup-models.md
│   │   │   ├── show-task.md
│   │   │   ├── smart-workflow.md
│   │   │   ├── sync-readme.md
│   │   │   ├── tm-main.md
│   │   │   ├── to-cancelled.md
│   │   │   ├── to-deferred.md
│   │   │   ├── to-done.md
│   │   │   ├── to-in-progress.md
│   │   │   ├── to-pending.md
│   │   │   ├── to-review.md
│   │   │   ├── update-single-task.md
│   │   │   ├── update-task.md
│   │   │   ├── update-tasks-from-id.md
│   │   │   ├── validate-dependencies.md
│   │   │   └── view-models.md
│   │   ├── mcp.json
│   │   └── package.json
│   ├── tm-bridge
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── src
│   │   │   ├── add-tag-bridge.ts
│   │   │   ├── bridge-types.ts
│   │   │   ├── bridge-utils.ts
│   │   │   ├── expand-bridge.ts
│   │   │   ├── index.ts
│   │   │   ├── tags-bridge.ts
│   │   │   ├── update-bridge.ts
│   │   │   └── use-tag-bridge.ts
│   │   └── tsconfig.json
│   └── tm-core
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── docs
│       │   └── listTasks-architecture.md
│       ├── package.json
│       ├── POC-STATUS.md
│       ├── README.md
│       ├── src
│       │   ├── common
│       │   │   ├── constants
│       │   │   │   ├── index.ts
│       │   │   │   ├── paths.ts
│       │   │   │   └── providers.ts
│       │   │   ├── errors
│       │   │   │   ├── index.ts
│       │   │   │   └── task-master-error.ts
│       │   │   ├── interfaces
│       │   │   │   ├── configuration.interface.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── storage.interface.ts
│       │   │   ├── logger
│       │   │   │   ├── factory.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── logger.spec.ts
│       │   │   │   └── logger.ts
│       │   │   ├── mappers
│       │   │   │   ├── TaskMapper.test.ts
│       │   │   │   └── TaskMapper.ts
│       │   │   ├── types
│       │   │   │   ├── database.types.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── legacy.ts
│       │   │   │   └── repository-types.ts
│       │   │   └── utils
│       │   │       ├── git-utils.ts
│       │   │       ├── id-generator.ts
│       │   │       ├── index.ts
│       │   │       ├── path-helpers.ts
│       │   │       ├── path-normalizer.spec.ts
│       │   │       ├── path-normalizer.ts
│       │   │       ├── project-root-finder.spec.ts
│       │   │       ├── project-root-finder.ts
│       │   │       ├── run-id-generator.spec.ts
│       │   │       └── run-id-generator.ts
│       │   ├── index.ts
│       │   ├── modules
│       │   │   ├── ai
│       │   │   │   ├── index.ts
│       │   │   │   ├── interfaces
│       │   │   │   │   └── ai-provider.interface.ts
│       │   │   │   └── providers
│       │   │   │       ├── base-provider.ts
│       │   │   │       └── index.ts
│       │   │   ├── auth
│       │   │   │   ├── auth-domain.spec.ts
│       │   │   │   ├── auth-domain.ts
│       │   │   │   ├── config.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── managers
│       │   │   │   │   ├── auth-manager.spec.ts
│       │   │   │   │   └── auth-manager.ts
│       │   │   │   ├── services
│       │   │   │   │   ├── context-store.ts
│       │   │   │   │   ├── oauth-service.ts
│       │   │   │   │   ├── organization.service.ts
│       │   │   │   │   ├── supabase-session-storage.spec.ts
│       │   │   │   │   └── supabase-session-storage.ts
│       │   │   │   └── types.ts
│       │   │   ├── briefs
│       │   │   │   ├── briefs-domain.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── services
│       │   │   │   │   └── brief-service.ts
│       │   │   │   ├── types.ts
│       │   │   │   └── utils
│       │   │   │       └── url-parser.ts
│       │   │   ├── commands
│       │   │   │   └── index.ts
│       │   │   ├── config
│       │   │   │   ├── config-domain.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── managers
│       │   │   │   │   ├── config-manager.spec.ts
│       │   │   │   │   └── config-manager.ts
│       │   │   │   └── services
│       │   │   │       ├── config-loader.service.spec.ts
│       │   │   │       ├── config-loader.service.ts
│       │   │   │       ├── config-merger.service.spec.ts
│       │   │   │       ├── config-merger.service.ts
│       │   │   │       ├── config-persistence.service.spec.ts
│       │   │   │       ├── config-persistence.service.ts
│       │   │   │       ├── environment-config-provider.service.spec.ts
│       │   │   │       ├── environment-config-provider.service.ts
│       │   │   │       ├── index.ts
│       │   │   │       ├── runtime-state-manager.service.spec.ts
│       │   │   │       └── runtime-state-manager.service.ts
│       │   │   ├── dependencies
│       │   │   │   └── index.ts
│       │   │   ├── execution
│       │   │   │   ├── executors
│       │   │   │   │   ├── base-executor.ts
│       │   │   │   │   ├── claude-executor.ts
│       │   │   │   │   └── executor-factory.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── services
│       │   │   │   │   └── executor-service.ts
│       │   │   │   └── types.ts
│       │   │   ├── git
│       │   │   │   ├── adapters
│       │   │   │   │   ├── git-adapter.test.ts
│       │   │   │   │   └── git-adapter.ts
│       │   │   │   ├── git-domain.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── services
│       │   │   │       ├── branch-name-generator.spec.ts
│       │   │   │       ├── branch-name-generator.ts
│       │   │   │       ├── commit-message-generator.test.ts
│       │   │   │       ├── commit-message-generator.ts
│       │   │   │       ├── scope-detector.test.ts
│       │   │   │       ├── scope-detector.ts
│       │   │   │       ├── template-engine.test.ts
│       │   │   │       └── template-engine.ts
│       │   │   ├── integration
│       │   │   │   ├── clients
│       │   │   │   │   ├── index.ts
│       │   │   │   │   └── supabase-client.ts
│       │   │   │   ├── integration-domain.ts
│       │   │   │   └── services
│       │   │   │       ├── export.service.ts
│       │   │   │       ├── task-expansion.service.ts
│       │   │   │       └── task-retrieval.service.ts
│       │   │   ├── reports
│       │   │   │   ├── index.ts
│       │   │   │   ├── managers
│       │   │   │   │   └── complexity-report-manager.ts
│       │   │   │   └── types.ts
│       │   │   ├── storage
│       │   │   │   ├── adapters
│       │   │   │   │   ├── activity-logger.ts
│       │   │   │   │   ├── api-storage.ts
│       │   │   │   │   └── file-storage
│       │   │   │   │       ├── file-operations.ts
│       │   │   │   │       ├── file-storage.ts
│       │   │   │   │       ├── format-handler.ts
│       │   │   │   │       ├── index.ts
│       │   │   │   │       └── path-resolver.ts
│       │   │   │   ├── index.ts
│       │   │   │   ├── services
│       │   │   │   │   └── storage-factory.ts
│       │   │   │   └── utils
│       │   │   │       └── api-client.ts
│       │   │   ├── tasks
│       │   │   │   ├── entities
│       │   │   │   │   └── task.entity.ts
│       │   │   │   ├── parser
│       │   │   │   │   └── index.ts
│       │   │   │   ├── repositories
│       │   │   │   │   ├── supabase
│       │   │   │   │   │   ├── dependency-fetcher.ts
│       │   │   │   │   │   ├── index.ts
│       │   │   │   │   │   └── supabase-repository.ts
│       │   │   │   │   └── task-repository.interface.ts
│       │   │   │   ├── services
│       │   │   │   │   ├── preflight-checker.service.ts
│       │   │   │   │   ├── tag.service.ts
│       │   │   │   │   ├── task-execution-service.ts
│       │   │   │   │   ├── task-loader.service.ts
│       │   │   │   │   └── task-service.ts
│       │   │   │   └── tasks-domain.ts
│       │   │   ├── ui
│       │   │   │   └── index.ts
│       │   │   └── workflow
│       │   │       ├── managers
│       │   │       │   ├── workflow-state-manager.spec.ts
│       │   │       │   └── workflow-state-manager.ts
│       │   │       ├── orchestrators
│       │   │       │   ├── workflow-orchestrator.test.ts
│       │   │       │   └── workflow-orchestrator.ts
│       │   │       ├── services
│       │   │       │   ├── test-result-validator.test.ts
│       │   │       │   ├── test-result-validator.ts
│       │   │       │   ├── test-result-validator.types.ts
│       │   │       │   ├── workflow-activity-logger.ts
│       │   │       │   └── workflow.service.ts
│       │   │       ├── types.ts
│       │   │       └── workflow-domain.ts
│       │   ├── subpath-exports.test.ts
│       │   ├── tm-core.ts
│       │   └── utils
│       │       └── time.utils.ts
│       ├── tests
│       │   ├── auth
│       │   │   └── auth-refresh.test.ts
│       │   ├── integration
│       │   │   ├── auth-token-refresh.test.ts
│       │   │   ├── list-tasks.test.ts
│       │   │   └── storage
│       │   │       └── activity-logger.test.ts
│       │   ├── mocks
│       │   │   └── mock-provider.ts
│       │   ├── setup.ts
│       │   └── unit
│       │       ├── base-provider.test.ts
│       │       ├── executor.test.ts
│       │       └── smoke.test.ts
│       ├── tsconfig.json
│       └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│   ├── create-worktree.sh
│   ├── dev.js
│   ├── init.js
│   ├── list-worktrees.sh
│   ├── modules
│   │   ├── ai-services-unified.js
│   │   ├── bridge-utils.js
│   │   ├── commands.js
│   │   ├── config-manager.js
│   │   ├── dependency-manager.js
│   │   ├── index.js
│   │   ├── prompt-manager.js
│   │   ├── supported-models.json
│   │   ├── sync-readme.js
│   │   ├── task-manager
│   │   │   ├── add-subtask.js
│   │   │   ├── add-task.js
│   │   │   ├── analyze-task-complexity.js
│   │   │   ├── clear-subtasks.js
│   │   │   ├── expand-all-tasks.js
│   │   │   ├── expand-task.js
│   │   │   ├── find-next-task.js
│   │   │   ├── generate-task-files.js
│   │   │   ├── is-task-dependent.js
│   │   │   ├── list-tasks.js
│   │   │   ├── migrate.js
│   │   │   ├── models.js
│   │   │   ├── move-task.js
│   │   │   ├── parse-prd
│   │   │   │   ├── index.js
│   │   │   │   ├── parse-prd-config.js
│   │   │   │   ├── parse-prd-helpers.js
│   │   │   │   ├── parse-prd-non-streaming.js
│   │   │   │   ├── parse-prd-streaming.js
│   │   │   │   └── parse-prd.js
│   │   │   ├── remove-subtask.js
│   │   │   ├── remove-task.js
│   │   │   ├── research.js
│   │   │   ├── response-language.js
│   │   │   ├── scope-adjustment.js
│   │   │   ├── set-task-status.js
│   │   │   ├── tag-management.js
│   │   │   ├── task-exists.js
│   │   │   ├── update-single-task-status.js
│   │   │   ├── update-subtask-by-id.js
│   │   │   ├── update-task-by-id.js
│   │   │   └── update-tasks.js
│   │   ├── task-manager.js
│   │   ├── ui.js
│   │   ├── update-config-tokens.js
│   │   ├── utils
│   │   │   ├── contextGatherer.js
│   │   │   ├── fuzzyTaskSearch.js
│   │   │   └── git-utils.js
│   │   └── utils.js
│   ├── task-complexity-report.json
│   ├── test-claude-errors.js
│   └── test-claude.js
├── sonar-project.properties
├── src
│   ├── ai-providers
│   │   ├── anthropic.js
│   │   ├── azure.js
│   │   ├── base-provider.js
│   │   ├── bedrock.js
│   │   ├── claude-code.js
│   │   ├── codex-cli.js
│   │   ├── gemini-cli.js
│   │   ├── google-vertex.js
│   │   ├── google.js
│   │   ├── grok-cli.js
│   │   ├── groq.js
│   │   ├── index.js
│   │   ├── lmstudio.js
│   │   ├── ollama.js
│   │   ├── openai-compatible.js
│   │   ├── openai.js
│   │   ├── openrouter.js
│   │   ├── perplexity.js
│   │   ├── xai.js
│   │   ├── zai-coding.js
│   │   └── zai.js
│   ├── constants
│   │   ├── commands.js
│   │   ├── paths.js
│   │   ├── profiles.js
│   │   ├── rules-actions.js
│   │   ├── task-priority.js
│   │   └── task-status.js
│   ├── profiles
│   │   ├── amp.js
│   │   ├── base-profile.js
│   │   ├── claude.js
│   │   ├── cline.js
│   │   ├── codex.js
│   │   ├── cursor.js
│   │   ├── gemini.js
│   │   ├── index.js
│   │   ├── kilo.js
│   │   ├── kiro.js
│   │   ├── opencode.js
│   │   ├── roo.js
│   │   ├── trae.js
│   │   ├── vscode.js
│   │   ├── windsurf.js
│   │   └── zed.js
│   ├── progress
│   │   ├── base-progress-tracker.js
│   │   ├── cli-progress-factory.js
│   │   ├── parse-prd-tracker.js
│   │   ├── progress-tracker-builder.js
│   │   └── tracker-ui.js
│   ├── prompts
│   │   ├── add-task.json
│   │   ├── analyze-complexity.json
│   │   ├── expand-task.json
│   │   ├── parse-prd.json
│   │   ├── README.md
│   │   ├── research.json
│   │   ├── schemas
│   │   │   ├── parameter.schema.json
│   │   │   ├── prompt-template.schema.json
│   │   │   ├── README.md
│   │   │   └── variant.schema.json
│   │   ├── update-subtask.json
│   │   ├── update-task.json
│   │   └── update-tasks.json
│   ├── provider-registry
│   │   └── index.js
│   ├── schemas
│   │   ├── add-task.js
│   │   ├── analyze-complexity.js
│   │   ├── base-schemas.js
│   │   ├── expand-task.js
│   │   ├── parse-prd.js
│   │   ├── registry.js
│   │   ├── update-subtask.js
│   │   ├── update-task.js
│   │   └── update-tasks.js
│   ├── task-master.js
│   ├── ui
│   │   ├── confirm.js
│   │   ├── indicators.js
│   │   └── parse-prd.js
│   └── utils
│       ├── asset-resolver.js
│       ├── create-mcp-config.js
│       ├── format.js
│       ├── getVersion.js
│       ├── logger-utils.js
│       ├── manage-gitignore.js
│       ├── path-utils.js
│       ├── profiles.js
│       ├── rule-transformer.js
│       ├── stream-parser.js
│       └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│   ├── e2e
│   │   ├── e2e_helpers.sh
│   │   ├── parse_llm_output.cjs
│   │   ├── run_e2e.sh
│   │   ├── run_fallback_verification.sh
│   │   └── test_llm_analysis.sh
│   ├── fixtures
│   │   ├── .taskmasterconfig
│   │   ├── sample-claude-response.js
│   │   ├── sample-prd.txt
│   │   └── sample-tasks.js
│   ├── helpers
│   │   └── tool-counts.js
│   ├── integration
│   │   ├── claude-code-error-handling.test.js
│   │   ├── claude-code-optional.test.js
│   │   ├── cli
│   │   │   ├── commands.test.js
│   │   │   ├── complex-cross-tag-scenarios.test.js
│   │   │   └── move-cross-tag.test.js
│   │   ├── manage-gitignore.test.js
│   │   ├── mcp-server
│   │   │   └── direct-functions.test.js
│   │   ├── move-task-cross-tag.integration.test.js
│   │   ├── move-task-simple.integration.test.js
│   │   ├── profiles
│   │   │   ├── amp-init-functionality.test.js
│   │   │   ├── claude-init-functionality.test.js
│   │   │   ├── cline-init-functionality.test.js
│   │   │   ├── codex-init-functionality.test.js
│   │   │   ├── cursor-init-functionality.test.js
│   │   │   ├── gemini-init-functionality.test.js
│   │   │   ├── opencode-init-functionality.test.js
│   │   │   ├── roo-files-inclusion.test.js
│   │   │   ├── roo-init-functionality.test.js
│   │   │   ├── rules-files-inclusion.test.js
│   │   │   ├── trae-init-functionality.test.js
│   │   │   ├── vscode-init-functionality.test.js
│   │   │   └── windsurf-init-functionality.test.js
│   │   └── providers
│   │       └── temperature-support.test.js
│   ├── manual
│   │   ├── progress
│   │   │   ├── parse-prd-analysis.js
│   │   │   ├── test-parse-prd.js
│   │   │   └── TESTING_GUIDE.md
│   │   └── prompts
│   │       ├── prompt-test.js
│   │       └── README.md
│   ├── README.md
│   ├── setup.js
│   └── unit
│       ├── ai-providers
│       │   ├── base-provider.test.js
│       │   ├── claude-code.test.js
│       │   ├── codex-cli.test.js
│       │   ├── gemini-cli.test.js
│       │   ├── lmstudio.test.js
│       │   ├── mcp-components.test.js
│       │   ├── openai-compatible.test.js
│       │   ├── openai.test.js
│       │   ├── provider-registry.test.js
│       │   ├── zai-coding.test.js
│       │   ├── zai-provider.test.js
│       │   ├── zai-schema-introspection.test.js
│       │   └── zai.test.js
│       ├── ai-services-unified.test.js
│       ├── commands.test.js
│       ├── config-manager.test.js
│       ├── config-manager.test.mjs
│       ├── dependency-manager.test.js
│       ├── init.test.js
│       ├── initialize-project.test.js
│       ├── kebab-case-validation.test.js
│       ├── manage-gitignore.test.js
│       ├── mcp
│       │   └── tools
│       │       ├── __mocks__
│       │       │   └── move-task.js
│       │       ├── add-task.test.js
│       │       ├── analyze-complexity.test.js
│       │       ├── expand-all.test.js
│       │       ├── get-tasks.test.js
│       │       ├── initialize-project.test.js
│       │       ├── move-task-cross-tag-options.test.js
│       │       ├── move-task-cross-tag.test.js
│       │       ├── remove-task.test.js
│       │       └── tool-registration.test.js
│       ├── mcp-providers
│       │   ├── mcp-components.test.js
│       │   └── mcp-provider.test.js
│       ├── parse-prd.test.js
│       ├── profiles
│       │   ├── amp-integration.test.js
│       │   ├── claude-integration.test.js
│       │   ├── cline-integration.test.js
│       │   ├── codex-integration.test.js
│       │   ├── cursor-integration.test.js
│       │   ├── gemini-integration.test.js
│       │   ├── kilo-integration.test.js
│       │   ├── kiro-integration.test.js
│       │   ├── mcp-config-validation.test.js
│       │   ├── opencode-integration.test.js
│       │   ├── profile-safety-check.test.js
│       │   ├── roo-integration.test.js
│       │   ├── rule-transformer-cline.test.js
│       │   ├── rule-transformer-cursor.test.js
│       │   ├── rule-transformer-gemini.test.js
│       │   ├── rule-transformer-kilo.test.js
│       │   ├── rule-transformer-kiro.test.js
│       │   ├── rule-transformer-opencode.test.js
│       │   ├── rule-transformer-roo.test.js
│       │   ├── rule-transformer-trae.test.js
│       │   ├── rule-transformer-vscode.test.js
│       │   ├── rule-transformer-windsurf.test.js
│       │   ├── rule-transformer-zed.test.js
│       │   ├── rule-transformer.test.js
│       │   ├── selective-profile-removal.test.js
│       │   ├── subdirectory-support.test.js
│       │   ├── trae-integration.test.js
│       │   ├── vscode-integration.test.js
│       │   ├── windsurf-integration.test.js
│       │   └── zed-integration.test.js
│       ├── progress
│       │   └── base-progress-tracker.test.js
│       ├── prompt-manager.test.js
│       ├── prompts
│       │   ├── expand-task-prompt.test.js
│       │   └── prompt-migration.test.js
│       ├── scripts
│       │   └── modules
│       │       ├── commands
│       │       │   ├── move-cross-tag.test.js
│       │       │   └── README.md
│       │       ├── dependency-manager
│       │       │   ├── circular-dependencies.test.js
│       │       │   ├── cross-tag-dependencies.test.js
│       │       │   └── fix-dependencies-command.test.js
│       │       ├── task-manager
│       │       │   ├── add-subtask.test.js
│       │       │   ├── add-task.test.js
│       │       │   ├── analyze-task-complexity.test.js
│       │       │   ├── clear-subtasks.test.js
│       │       │   ├── complexity-report-tag-isolation.test.js
│       │       │   ├── expand-all-tasks.test.js
│       │       │   ├── expand-task.test.js
│       │       │   ├── find-next-task.test.js
│       │       │   ├── generate-task-files.test.js
│       │       │   ├── list-tasks.test.js
│       │       │   ├── models-baseurl.test.js
│       │       │   ├── move-task-cross-tag.test.js
│       │       │   ├── move-task.test.js
│       │       │   ├── parse-prd-schema.test.js
│       │       │   ├── parse-prd.test.js
│       │       │   ├── remove-subtask.test.js
│       │       │   ├── remove-task.test.js
│       │       │   ├── research.test.js
│       │       │   ├── scope-adjustment.test.js
│       │       │   ├── set-task-status.test.js
│       │       │   ├── setup.js
│       │       │   ├── update-single-task-status.test.js
│       │       │   ├── update-subtask-by-id.test.js
│       │       │   ├── update-task-by-id.test.js
│       │       │   └── update-tasks.test.js
│       │       ├── ui
│       │       │   └── cross-tag-error-display.test.js
│       │       └── utils-tag-aware-paths.test.js
│       ├── task-finder.test.js
│       ├── task-manager
│       │   ├── clear-subtasks.test.js
│       │   ├── move-task.test.js
│       │   ├── tag-boundary.test.js
│       │   └── tag-management.test.js
│       ├── task-master.test.js
│       ├── ui
│       │   └── indicators.test.js
│       ├── ui.test.js
│       ├── utils-strip-ansi.test.js
│       └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```

# Files

--------------------------------------------------------------------------------
/apps/cli/src/ui/components/task-detail.component.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Task detail component for show command
  3 |  * Displays detailed task information in a structured format
  4 |  */
  5 | 
  6 | import type { StorageType, Subtask, Task } from '@tm/core';
  7 | import boxen from 'boxen';
  8 | import chalk from 'chalk';
  9 | import Table from 'cli-table3';
 10 | import { MarkedExtension, marked } from 'marked';
 11 | import { markedTerminal } from 'marked-terminal';
 12 | import {
 13 | 	getComplexityWithColor,
 14 | 	getPriorityWithColor,
 15 | 	getStatusWithColor
 16 | } from '../../utils/ui.js';
 17 | 
 18 | // Configure marked to use terminal renderer with subtle colors
 19 | marked.use(
 20 | 	markedTerminal({
 21 | 		// More subtle colors that match the overall design
 22 | 		code: (code: string) => {
 23 | 			// Custom code block handler to preserve formatting
 24 | 			return code
 25 | 				.split('\n')
 26 | 				.map((line) => '    ' + chalk.cyan(line))
 27 | 				.join('\n');
 28 | 		},
 29 | 		blockquote: chalk.gray.italic,
 30 | 		html: chalk.gray,
 31 | 		heading: chalk.white.bold, // White bold for headings
 32 | 		hr: chalk.gray,
 33 | 		listitem: chalk.white, // White for list items
 34 | 		paragraph: chalk.white, // White for paragraphs (default text color)
 35 | 		strong: chalk.white.bold, // White bold for strong text
 36 | 		em: chalk.white.italic, // White italic for emphasis
 37 | 		codespan: chalk.cyan, // Cyan for inline code (no background)
 38 | 		del: chalk.dim.strikethrough,
 39 | 		link: chalk.blue,
 40 | 		href: chalk.blue.underline,
 41 | 		// Add more explicit code block handling
 42 | 		showSectionPrefix: false,
 43 | 		unescape: true,
 44 | 		emoji: false,
 45 | 		// Try to preserve whitespace in code blocks
 46 | 		tab: 4,
 47 | 		width: 120
 48 | 	}) as MarkedExtension
 49 | );
 50 | 
 51 | // Also set marked options to preserve whitespace
 52 | marked.setOptions({
 53 | 	breaks: true,
 54 | 	gfm: true
 55 | });
 56 | 
 57 | /**
 58 |  * Display the task header with tag
 59 |  */
 60 | export function displayTaskHeader(
 61 | 	taskId: string | number,
 62 | 	title: string
 63 | ): void {
 64 | 	// Display task header box
 65 | 	console.log(
 66 | 		boxen(chalk.white.bold(`Task: #${taskId} - ${title}`), {
 67 | 			padding: { top: 0, bottom: 0, left: 1, right: 1 },
 68 | 			borderColor: 'blue',
 69 | 			borderStyle: 'round'
 70 | 		})
 71 | 	);
 72 | }
 73 | 
 74 | /**
 75 |  * Display task properties in a table format
 76 |  */
 77 | export function displayTaskProperties(
 78 | 	task: Task | Subtask,
 79 | 	originalTaskId?: string
 80 | ): void {
 81 | 	const terminalWidth = process.stdout.columns * 0.95 || 100;
 82 | 	// Create table for task properties - simple 2-column layout
 83 | 	const table = new Table({
 84 | 		head: [],
 85 | 		style: {
 86 | 			head: [],
 87 | 			border: ['grey']
 88 | 		},
 89 | 		colWidths: [
 90 | 			Math.floor(terminalWidth * 0.2),
 91 | 			Math.floor(terminalWidth * 0.8)
 92 | 		],
 93 | 		wordWrap: true
 94 | 	});
 95 | 
 96 | 	const deps =
 97 | 		task.dependencies && task.dependencies.length > 0
 98 | 			? task.dependencies.map((d) => String(d)).join(', ')
 99 | 			: 'None';
100 | 
101 | 	// Use originalTaskId if provided (for subtasks like "104.1")
102 | 	const displayId = originalTaskId || String(task.id);
103 | 
104 | 	// Build the left column (labels) and right column (values)
105 | 	const labels = [
106 | 		chalk.cyan('ID:'),
107 | 		chalk.cyan('Title:'),
108 | 		chalk.cyan('Status:'),
109 | 		chalk.cyan('Priority:'),
110 | 		chalk.cyan('Dependencies:'),
111 | 		chalk.cyan('Complexity:'),
112 | 		chalk.cyan('Description:')
113 | 	].join('\n');
114 | 
115 | 	const values = [
116 | 		displayId,
117 | 		task.title,
118 | 		getStatusWithColor(task.status),
119 | 		getPriorityWithColor(task.priority),
120 | 		deps,
121 | 		typeof task.complexity === 'number'
122 | 			? getComplexityWithColor(task.complexity)
123 | 			: chalk.gray('N/A'),
124 | 		task.description || ''
125 | 	].join('\n');
126 | 
127 | 	table.push([labels, values]);
128 | 
129 | 	console.log(table.toString());
130 | }
131 | 
132 | /**
133 |  * Display implementation details in a box
134 |  */
135 | export function displayImplementationDetails(details: string): void {
136 | 	// Handle all escaped characters properly
137 | 	const cleanDetails = details
138 | 		.replace(/\\n/g, '\n') // Convert \n to actual newlines
139 | 		.replace(/\\t/g, '\t') // Convert \t to actual tabs
140 | 		.replace(/\\"/g, '"') // Convert \" to actual quotes
141 | 		.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
142 | 
143 | 	const terminalWidth = process.stdout.columns * 0.95 || 100;
144 | 
145 | 	// Parse markdown to terminal-friendly format
146 | 	const markdownResult = marked(cleanDetails);
147 | 	const formattedDetails =
148 | 		typeof markdownResult === 'string' ? markdownResult.trim() : cleanDetails; // Fallback to original if Promise
149 | 
150 | 	console.log(
151 | 		boxen(
152 | 			chalk.white.bold('Implementation Details:') + '\n\n' + formattedDetails,
153 | 			{
154 | 				padding: 1,
155 | 				borderStyle: 'round',
156 | 				borderColor: 'cyan', // Changed to cyan to match the original
157 | 				width: terminalWidth // Fixed width to match the original
158 | 			}
159 | 		)
160 | 	);
161 | }
162 | 
163 | /**
164 |  * Display test strategy in a box
165 |  */
166 | export function displayTestStrategy(testStrategy: string): void {
167 | 	// Handle all escaped characters properly (same as implementation details)
168 | 	const cleanStrategy = testStrategy
169 | 		.replace(/\\n/g, '\n') // Convert \n to actual newlines
170 | 		.replace(/\\t/g, '\t') // Convert \t to actual tabs
171 | 		.replace(/\\"/g, '"') // Convert \" to actual quotes
172 | 		.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
173 | 
174 | 	const terminalWidth = process.stdout.columns * 0.95 || 100;
175 | 
176 | 	// Parse markdown to terminal-friendly format (same as implementation details)
177 | 	const markdownResult = marked(cleanStrategy);
178 | 	const formattedStrategy =
179 | 		typeof markdownResult === 'string' ? markdownResult.trim() : cleanStrategy; // Fallback to original if Promise
180 | 
181 | 	console.log(
182 | 		boxen(chalk.white.bold('Test Strategy:') + '\n\n' + formattedStrategy, {
183 | 			padding: 1,
184 | 			borderStyle: 'round',
185 | 			borderColor: 'cyan', // Changed to cyan to match implementation details
186 | 			width: terminalWidth
187 | 		})
188 | 	);
189 | }
190 | 
191 | /**
192 |  * Display subtasks in a table format
193 |  */
194 | export function displaySubtasks(
195 | 	subtasks: Array<{
196 | 		id: string | number;
197 | 		title: string;
198 | 		status: any;
199 | 		description?: string;
200 | 		dependencies?: string[];
201 | 	}>,
202 | 	parentTaskId?: string | number,
203 | 	storageType?: Exclude<StorageType, 'auto'>
204 | ): void {
205 | 	const terminalWidth = process.stdout.columns * 0.95 || 100;
206 | 	// Display subtasks header
207 | 	console.log(
208 | 		boxen(chalk.magenta.bold('Subtasks'), {
209 | 			padding: { top: 0, bottom: 0, left: 1, right: 1 },
210 | 			borderColor: 'magenta',
211 | 			borderStyle: 'round',
212 | 			margin: { top: 1, bottom: 0 }
213 | 		})
214 | 	);
215 | 
216 | 	// Create subtasks table
217 | 	const table = new Table({
218 | 		head: [
219 | 			chalk.magenta.bold('ID'),
220 | 			chalk.magenta.bold('Status'),
221 | 			chalk.magenta.bold('Title'),
222 | 			chalk.magenta.bold('Deps')
223 | 		],
224 | 		style: {
225 | 			head: [],
226 | 			border: ['grey']
227 | 		},
228 | 		colWidths: [
229 | 			Math.floor(terminalWidth * 0.1),
230 | 			Math.floor(terminalWidth * 0.15),
231 | 			Math.floor(terminalWidth * 0.6),
232 | 			Math.floor(terminalWidth * 0.15)
233 | 		],
234 | 		wordWrap: true
235 | 	});
236 | 
237 | 	subtasks.forEach((subtask) => {
238 | 		// Format subtask ID based on storage type:
239 | 		// - File storage: Show parent prefix (e.g., 10.1, 10.2)
240 | 		// - API storage: Show subtask ID only (e.g., 1, 2)
241 | 		const subtaskId =
242 | 			storageType === 'file' && parentTaskId
243 | 				? `${parentTaskId}.${subtask.id}`
244 | 				: String(subtask.id);
245 | 
246 | 		// Format dependencies
247 | 		const deps =
248 | 			subtask.dependencies && subtask.dependencies.length > 0
249 | 				? subtask.dependencies.join(', ')
250 | 				: 'None';
251 | 
252 | 		table.push([
253 | 			subtaskId,
254 | 			getStatusWithColor(subtask.status),
255 | 			subtask.title,
256 | 			deps
257 | 		]);
258 | 	});
259 | 
260 | 	console.log(table.toString());
261 | }
262 | 
263 | /**
264 |  * Display suggested actions
265 |  */
266 | export function displaySuggestedActions(taskId: string | number): void {
267 | 	console.log(
268 | 		boxen(
269 | 			chalk.white.bold('Suggested Actions:') +
270 | 				'\n\n' +
271 | 				`${chalk.cyan('1.')} Run ${chalk.yellow(`task-master set-status --id=${taskId} --status=in-progress`)} to start working\n` +
272 | 				`${chalk.cyan('2.')} Run ${chalk.yellow(`task-master expand --id=${taskId}`)} to break down into subtasks\n` +
273 | 				`${chalk.cyan('3.')} Run ${chalk.yellow(`task-master update-task --id=${taskId} --prompt="..."`)} to update details`,
274 | 			{
275 | 				padding: 1,
276 | 				margin: { top: 1 },
277 | 				borderStyle: 'round',
278 | 				borderColor: 'green',
279 | 				width: process.stdout.columns * 0.95 || 100
280 | 			}
281 | 		)
282 | 	);
283 | }
284 | 
285 | /**
286 |  * Display complete task details - used by both show and start commands
287 |  */
288 | export function displayTaskDetails(
289 | 	task: Task | Subtask,
290 | 	options?: {
291 | 		statusFilter?: string;
292 | 		showSuggestedActions?: boolean;
293 | 		customHeader?: string;
294 | 		headerColor?: string;
295 | 		originalTaskId?: string;
296 | 		storageType?: Exclude<StorageType, 'auto'>;
297 | 	}
298 | ): void {
299 | 	const {
300 | 		statusFilter,
301 | 		showSuggestedActions = false,
302 | 		customHeader,
303 | 		headerColor = 'blue',
304 | 		originalTaskId,
305 | 		storageType
306 | 	} = options || {};
307 | 
308 | 	// Display header - either custom or default
309 | 	if (customHeader) {
310 | 		console.log(
311 | 			boxen(chalk.white.bold(customHeader), {
312 | 				padding: { top: 0, bottom: 0, left: 1, right: 1 },
313 | 				borderColor: headerColor,
314 | 				borderStyle: 'round',
315 | 				margin: { top: 1 }
316 | 			})
317 | 		);
318 | 	} else {
319 | 		// Use originalTaskId if provided (for subtasks like "104.1")
320 | 		const displayId = originalTaskId || task.id;
321 | 		displayTaskHeader(displayId, task.title);
322 | 	}
323 | 
324 | 	// Display task properties in table format
325 | 	displayTaskProperties(task, originalTaskId);
326 | 
327 | 	// Display implementation details if available
328 | 	if (task.details) {
329 | 		console.log(); // Empty line for spacing
330 | 		displayImplementationDetails(task.details);
331 | 	}
332 | 
333 | 	// Display test strategy if available
334 | 	if ('testStrategy' in task && task.testStrategy) {
335 | 		console.log(); // Empty line for spacing
336 | 		displayTestStrategy(task.testStrategy as string);
337 | 	}
338 | 
339 | 	// Display subtasks if available
340 | 	if (task.subtasks && task.subtasks.length > 0) {
341 | 		// Filter subtasks by status if provided
342 | 		const filteredSubtasks = statusFilter
343 | 			? task.subtasks.filter((sub) => sub.status === statusFilter)
344 | 			: task.subtasks;
345 | 
346 | 		if (filteredSubtasks.length === 0 && statusFilter) {
347 | 			console.log(); // Empty line for spacing
348 | 			console.log(chalk.gray(`  No subtasks with status '${statusFilter}'`));
349 | 		} else if (filteredSubtasks.length > 0) {
350 | 			console.log(); // Empty line for spacing
351 | 			displaySubtasks(filteredSubtasks, task.id, storageType);
352 | 		}
353 | 	}
354 | 
355 | 	// Display suggested actions if requested
356 | 	if (showSuggestedActions) {
357 | 		console.log(); // Empty line for spacing
358 | 		const actionTaskId = originalTaskId || task.id;
359 | 		displaySuggestedActions(actionTaskId);
360 | 	}
361 | }
362 | 
```

--------------------------------------------------------------------------------
/src/utils/profiles.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Profiles Utility
  3 |  * Consolidated utilities for profile detection, setup, and summary generation
  4 |  */
  5 | import fs from 'fs';
  6 | import path from 'path';
  7 | import inquirer from 'inquirer';
  8 | import chalk from 'chalk';
  9 | import boxen from 'boxen';
 10 | import { log } from '../../scripts/modules/utils.js';
 11 | import { getRulesProfile } from './rule-transformer.js';
 12 | import { RULE_PROFILES } from '../constants/profiles.js';
 13 | 
 14 | // =============================================================================
 15 | // PROFILE DETECTION
 16 | // =============================================================================
 17 | 
 18 | /**
 19 |  * Get the display name for a profile
 20 |  * @param {string} profileName - The profile name
 21 |  * @returns {string} - The display name
 22 |  */
 23 | export function getProfileDisplayName(profileName) {
 24 | 	try {
 25 | 		const profile = getRulesProfile(profileName);
 26 | 		return profile.displayName || profileName;
 27 | 	} catch (error) {
 28 | 		return profileName;
 29 | 	}
 30 | }
 31 | 
 32 | /**
 33 |  * Get installed profiles in the project directory
 34 |  * @param {string} projectRoot - Project directory path
 35 |  * @returns {string[]} - Array of installed profile names
 36 |  */
 37 | export function getInstalledProfiles(projectRoot) {
 38 | 	const installedProfiles = [];
 39 | 
 40 | 	for (const profileName of RULE_PROFILES) {
 41 | 		try {
 42 | 			const profile = getRulesProfile(profileName);
 43 | 			const profileDir = path.join(projectRoot, profile.profileDir);
 44 | 
 45 | 			// Check if profile directory exists (skip root directory check)
 46 | 			if (profile.profileDir === '.' || fs.existsSync(profileDir)) {
 47 | 				// Check if any files from the profile's fileMap exist
 48 | 				const rulesDir = path.join(projectRoot, profile.rulesDir);
 49 | 				if (fs.existsSync(rulesDir)) {
 50 | 					const ruleFiles = Object.values(profile.fileMap);
 51 | 					const hasRuleFiles = ruleFiles.some((ruleFile) =>
 52 | 						fs.existsSync(path.join(rulesDir, ruleFile))
 53 | 					);
 54 | 					if (hasRuleFiles) {
 55 | 						installedProfiles.push(profileName);
 56 | 					}
 57 | 				}
 58 | 			}
 59 | 		} catch (error) {
 60 | 			// Skip profiles that can't be loaded
 61 | 		}
 62 | 	}
 63 | 
 64 | 	return installedProfiles;
 65 | }
 66 | 
 67 | /**
 68 |  * Check if removing specified profiles would leave no profiles installed
 69 |  * @param {string} projectRoot - Project root directory
 70 |  * @param {string[]} profilesToRemove - Array of profile names to remove
 71 |  * @returns {boolean} - True if removal would leave no profiles
 72 |  */
 73 | export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) {
 74 | 	const installedProfiles = getInstalledProfiles(projectRoot);
 75 | 
 76 | 	// If no profiles are currently installed, removal cannot leave no profiles
 77 | 	if (installedProfiles.length === 0) {
 78 | 		return false;
 79 | 	}
 80 | 
 81 | 	const remainingProfiles = installedProfiles.filter(
 82 | 		(profile) => !profilesToRemove.includes(profile)
 83 | 	);
 84 | 	return remainingProfiles.length === 0;
 85 | }
 86 | 
 87 | // =============================================================================
 88 | // PROFILE SETUP
 89 | // =============================================================================
 90 | 
 91 | // Note: Profile choices are now generated dynamically within runInteractiveProfilesSetup()
 92 | // to ensure proper alphabetical sorting and pagination configuration
 93 | 
 94 | /**
 95 |  * Launches an interactive prompt for selecting which rule profiles to include in your project.
 96 |  *
 97 |  * This function dynamically lists all available profiles (from RULE_PROFILES) and presents them as checkboxes.
 98 |  * The user must select at least one profile (no defaults are pre-selected). The result is an array of selected profile names.
 99 |  *
100 |  * Used by both project initialization (init) and the CLI 'task-master rules setup' command.
101 |  *
102 |  * @returns {Promise<string[]>} Array of selected profile names (e.g., ['cursor', 'windsurf'])
103 |  */
104 | export async function runInteractiveProfilesSetup() {
105 | 	// Generate the profile list dynamically with proper display names, alphabetized
106 | 	const profileDescriptions = RULE_PROFILES.map((profileName) => {
107 | 		const displayName = getProfileDisplayName(profileName);
108 | 		const profile = getRulesProfile(profileName);
109 | 
110 | 		// Determine description based on profile capabilities
111 | 		let description;
112 | 		const hasRules = Object.keys(profile.fileMap).length > 0;
113 | 		const hasMcpConfig = profile.mcpConfig === true;
114 | 
115 | 		if (!profile.includeDefaultRules) {
116 | 			// Integration guide profiles (claude, codex, gemini, opencode, zed, amp) - don't include standard coding rules
117 | 			if (profileName === 'claude') {
118 | 				description = 'Integration guide with Task Master slash commands';
119 | 			} else if (profileName === 'codex') {
120 | 				description = 'Comprehensive Task Master integration guide';
121 | 			} else if (hasMcpConfig) {
122 | 				description = 'Integration guide and MCP config';
123 | 			} else {
124 | 				description = 'Integration guide';
125 | 			}
126 | 		} else if (hasRules && hasMcpConfig) {
127 | 			// Full rule profiles with MCP config
128 | 			if (profileName === 'roo') {
129 | 				description = 'Rule profile, MCP config, and agent modes';
130 | 			} else {
131 | 				description = 'Rule profile and MCP config';
132 | 			}
133 | 		} else if (hasRules) {
134 | 			// Rule profiles without MCP config
135 | 			description = 'Rule profile';
136 | 		}
137 | 
138 | 		return {
139 | 			profileName,
140 | 			displayName,
141 | 			description
142 | 		};
143 | 	}).sort((a, b) => a.displayName.localeCompare(b.displayName));
144 | 
145 | 	const profileListText = profileDescriptions
146 | 		.map(
147 | 			({ displayName, description }) =>
148 | 				`${chalk.white('• ')}${chalk.yellow(displayName)}${chalk.white(` - ${description}`)}`
149 | 		)
150 | 		.join('\n');
151 | 
152 | 	console.log(
153 | 		boxen(
154 | 			`${chalk.white.bold('Rule Profiles Setup')}\n\n${chalk.white(
155 | 				'Rule profiles help enforce best practices and conventions for Task Master.\n' +
156 | 					'Each profile provides coding guidelines tailored for specific AI coding environments.\n\n'
157 | 			)}${chalk.cyan('Available Profiles:')}\n${profileListText}`,
158 | 			{
159 | 				padding: 1,
160 | 				borderColor: 'blue',
161 | 				borderStyle: 'round',
162 | 				margin: { top: 1, bottom: 1 }
163 | 			}
164 | 		)
165 | 	);
166 | 
167 | 	// Generate choices in the same order as the display text above
168 | 	const sortedChoices = profileDescriptions.map(
169 | 		({ profileName, displayName }) => ({
170 | 			name: displayName,
171 | 			value: profileName
172 | 		})
173 | 	);
174 | 
175 | 	const ruleProfilesQuestion = {
176 | 		type: 'checkbox',
177 | 		name: 'ruleProfiles',
178 | 		message: 'Which rule profiles would you like to add to your project?',
179 | 		choices: sortedChoices,
180 | 		pageSize: sortedChoices.length, // Show all options without pagination
181 | 		loop: false, // Disable loop scrolling
182 | 		validate: (input) => input.length > 0 || 'You must select at least one.'
183 | 	};
184 | 	const { ruleProfiles } = await inquirer.prompt([ruleProfilesQuestion]);
185 | 	return ruleProfiles;
186 | }
187 | 
188 | // =============================================================================
189 | // PROFILE SUMMARY
190 | // =============================================================================
191 | 
192 | /**
193 |  * Generate appropriate summary message for a profile based on its type
194 |  * @param {string} profileName - Name of the profile
195 |  * @param {Object} addResult - Result object with success/failed counts
196 |  * @returns {string} Formatted summary message
197 |  */
198 | export function generateProfileSummary(profileName, addResult) {
199 | 	const profileConfig = getRulesProfile(profileName);
200 | 
201 | 	if (!profileConfig.includeDefaultRules) {
202 | 		// Integration guide profiles (claude, codex, gemini, amp)
203 | 		return `Summary for ${profileName}: Integration guide installed.`;
204 | 	} else {
205 | 		// Rule profiles with coding guidelines
206 | 		return `Summary for ${profileName}: ${addResult.success} files processed, ${addResult.failed} failed.`;
207 | 	}
208 | }
209 | 
210 | /**
211 |  * Generate appropriate summary message for profile removal
212 |  * @param {string} profileName - Name of the profile
213 |  * @param {Object} removeResult - Result object from removal operation
214 |  * @returns {string} Formatted summary message
215 |  */
216 | export function generateProfileRemovalSummary(profileName, removeResult) {
217 | 	if (removeResult.skipped) {
218 | 		return `Summary for ${profileName}: Skipped (default or protected files)`;
219 | 	}
220 | 
221 | 	if (removeResult.error && !removeResult.success) {
222 | 		return `Summary for ${profileName}: Failed to remove - ${removeResult.error}`;
223 | 	}
224 | 
225 | 	const profileConfig = getRulesProfile(profileName);
226 | 
227 | 	if (!profileConfig.includeDefaultRules) {
228 | 		// Integration guide profiles (claude, codex, gemini, amp)
229 | 		const baseMessage = `Summary for ${profileName}: Integration guide removed`;
230 | 		if (removeResult.notice) {
231 | 			return `${baseMessage} (${removeResult.notice})`;
232 | 		}
233 | 		return baseMessage;
234 | 	} else {
235 | 		// Rule profiles with coding guidelines
236 | 		const baseMessage = `Summary for ${profileName}: Rule profile removed`;
237 | 		if (removeResult.notice) {
238 | 			return `${baseMessage} (${removeResult.notice})`;
239 | 		}
240 | 		return baseMessage;
241 | 	}
242 | }
243 | 
244 | /**
245 |  * Categorize profiles and generate final summary statistics
246 |  * @param {Array} addResults - Array of add result objects
247 |  * @returns {Object} Object with categorized profiles and totals
248 |  */
249 | export function categorizeProfileResults(addResults) {
250 | 	const successfulProfiles = [];
251 | 	let totalSuccess = 0;
252 | 	let totalFailed = 0;
253 | 
254 | 	addResults.forEach((r) => {
255 | 		totalSuccess += r.success;
256 | 		totalFailed += r.failed;
257 | 
258 | 		// All profiles are considered successful if they completed without major errors
259 | 		if (r.success > 0 || r.failed === 0) {
260 | 			successfulProfiles.push(r.profileName);
261 | 		}
262 | 	});
263 | 
264 | 	return {
265 | 		successfulProfiles,
266 | 		allSuccessfulProfiles: successfulProfiles,
267 | 		totalSuccess,
268 | 		totalFailed
269 | 	};
270 | }
271 | 
272 | /**
273 |  * Categorize removal results and generate final summary statistics
274 |  * @param {Array} removalResults - Array of removal result objects
275 |  * @returns {Object} Object with categorized removal results
276 |  */
277 | export function categorizeRemovalResults(removalResults) {
278 | 	const successfulRemovals = [];
279 | 	const skippedRemovals = [];
280 | 	const failedRemovals = [];
281 | 	const removalsWithNotices = [];
282 | 
283 | 	removalResults.forEach((result) => {
284 | 		if (result.success) {
285 | 			successfulRemovals.push(result.profileName);
286 | 		} else if (result.skipped) {
287 | 			skippedRemovals.push(result.profileName);
288 | 		} else if (result.error) {
289 | 			failedRemovals.push(result);
290 | 		}
291 | 
292 | 		if (result.notice) {
293 | 			removalsWithNotices.push(result);
294 | 		}
295 | 	});
296 | 
297 | 	return {
298 | 		successfulRemovals,
299 | 		skippedRemovals,
300 | 		failedRemovals,
301 | 		removalsWithNotices
302 | 	};
303 | }
304 | 
```

--------------------------------------------------------------------------------
/tests/e2e/e2e_helpers.sh:
--------------------------------------------------------------------------------

```bash
  1 | #!/bin/bash
  2 | 
  3 | # --- LLM Analysis Helper Function ---
  4 | # This function should be sourced by the main E2E script or test scripts.
  5 | # It requires curl and jq to be installed.
  6 | # It expects the project root path to be passed as the second argument.
  7 | 
  8 | # --- New Function: extract_and_sum_cost ---
  9 | # Takes a string containing command output.
 10 | # Extracts costs (lines with "Est. Cost: $X.YYYYYY" or similar from telemetry output)
 11 | # from the output, sums them, and adds them to the GLOBAL total_e2e_cost variable.
 12 | extract_and_sum_cost() {
 13 |   local command_output="$1"
 14 |   # Ensure total_e2e_cost is treated as a number, default to 0.0 if not set or invalid
 15 |   if ! [[ "$total_e2e_cost" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
 16 |     total_e2e_cost="0.0"
 17 |   fi
 18 | 
 19 |   local extracted_cost_sum="0.0"
 20 | 
 21 |   # Grep for lines containing "Est. Cost: $", then extract the numeric value.
 22 |   # Example line: │     Est. Cost: $0.093549                       │
 23 |   # Accumulate all costs found in the command_output
 24 |   while IFS= read -r line; do
 25 |     # Extract the numeric part after 'Est. Cost: $' and before any trailing spaces/chars
 26 |     cost_value=$(echo "$line" | grep -o -E 'Est\. Cost: \$([0-9]+\.[0-9]+)' | sed -E 's/Est\. Cost: \$//g')
 27 |     if [[ -n "$cost_value" && "$cost_value" =~ ^[0-9]+\.[0-9]+$ ]]; then
 28 |       # echo "[DEBUG] Found cost value: $cost_value in line: '$line'" # For debugging
 29 |       extracted_cost_sum=$(echo "$extracted_cost_sum + $cost_value" | bc)
 30 |     # else # For debugging
 31 |       # echo "[DEBUG] No valid cost value found or extracted in line: '$line' (extracted: '$cost_value')" # For debugging
 32 |     fi
 33 |   done < <(echo "$command_output" | grep -E 'Est\. Cost: \$')
 34 | 
 35 |   # echo "[DEBUG] Extracted sum from this command output: $extracted_cost_sum" # For debugging
 36 |   if (( $(echo "$extracted_cost_sum > 0" | bc -l) )); then
 37 |     total_e2e_cost=$(echo "$total_e2e_cost + $extracted_cost_sum" | bc)
 38 |     # echo "[DEBUG] Updated global total_e2e_cost: $total_e2e_cost" # For debugging
 39 |   fi
 40 |   # No echo here, the function modifies a global variable.
 41 | }
 42 | export -f extract_and_sum_cost # Export for use in other scripts if sourced
 43 | 
 44 | analyze_log_with_llm() {
 45 |   local log_file="$1"
 46 |   local project_root="$2" # Expect project root as the second argument
 47 | 
 48 |   if [ -z "$project_root" ]; then
 49 |       echo "[HELPER_ERROR] Project root argument is missing. Skipping LLM analysis." >&2
 50 |       return 1
 51 |   fi
 52 | 
 53 |   local env_file="${project_root}/.env" # Path to .env in project root
 54 |   local supported_models_file="${project_root}/scripts/modules/supported-models.json"
 55 | 
 56 |   local provider_summary_log="provider_add_task_summary.log" # File summarizing provider test outcomes
 57 |   local api_key=""
 58 |   local api_endpoint="https://api.anthropic.com/v1/messages"
 59 |   local api_key_name="ANTHROPIC_API_KEY"
 60 |   local llm_analysis_model_id="claude-3-7-sonnet-20250219" # Model used for this analysis
 61 |   local llm_analysis_provider="anthropic"
 62 | 
 63 |   echo "" # Add a newline before analysis starts
 64 | 
 65 |   if ! command -v jq &> /dev/null; then
 66 |     echo "[HELPER_ERROR] LLM Analysis requires 'jq'. Skipping analysis." >&2
 67 |     return 1
 68 |   fi
 69 |   if ! command -v curl &> /dev/null; then
 70 |     echo "[HELPER_ERROR] LLM Analysis requires 'curl'. Skipping analysis." >&2
 71 |     return 1
 72 |   fi
 73 |   if ! command -v bc &> /dev/null; then
 74 |     echo "[HELPER_ERROR] LLM Analysis requires 'bc' for cost calculation. Skipping analysis." >&2
 75 |     return 1
 76 |   fi
 77 | 
 78 |   if [ -f "$env_file" ]; then
 79 |     api_key=$(grep "^${api_key_name}=" "$env_file" | sed -e "s/^${api_key_name}=//" -e 's/^[[:space:]"]*//' -e 's/[[:space:]"]*$//')
 80 |   fi
 81 | 
 82 |   if [ -z "$api_key" ]; then
 83 |     echo "[HELPER_ERROR] ${api_key_name} not found or empty in project root .env file ($env_file). Skipping LLM analysis." >&2
 84 |     return 1
 85 |   fi
 86 | 
 87 |   if [ ! -f "$log_file" ]; then
 88 |     echo "[HELPER_ERROR] Log file not found: $log_file (PWD: $(pwd)). Check path passed to function. Skipping LLM analysis." >&2
 89 |     return 1
 90 |   fi
 91 | 
 92 |   local log_content
 93 |   log_content=$(cat "$log_file") || {
 94 |     echo "[HELPER_ERROR] Failed to read log file: $log_file. Skipping LLM analysis." >&2
 95 |     return 1
 96 |   }
 97 | 
 98 |   read -r -d '' prompt_template <<'EOF'
 99 | Analyze the following E2E test log for the task-master tool. The log contains output from various 'task-master' commands executed sequentially.
100 | 
101 | Your goal is to:
102 | 1. Verify if the key E2E steps completed successfully based on the log messages (e.g., init, parse PRD, list tasks, analyze complexity, expand task, set status, manage models, add/remove dependencies, add/update/remove tasks/subtasks, generate files).
103 | 2. **Specifically analyze the Multi-Provider Add-Task Test Sequence:**
104 |    a. Identify which providers were tested for `add-task`. Look for log steps like "Testing Add-Task with Provider: ..." and the summary log 'provider_add_task_summary.log'.
105 |    b. For each tested provider, determine if `add-task` succeeded or failed. Note the created task ID if successful.
106 |    c. Review the corresponding `add_task_show_output_<provider>_id_<id>.log` file (if created) for each successful `add-task` execution.
107 |    d. **Compare the quality and completeness** of the task generated by each successful provider based on their `show` output. Assign a score (e.g., 1-10, 10 being best) based on relevance to the prompt, detail level, and correctness.
108 |    e. Note any providers where `add-task` failed or where the task ID could not be extracted.
109 | 3. Identify any general explicit "[ERROR]" messages or stack traces throughout the *entire* log.
110 | 4. Identify any potential warnings or unusual output that might indicate a problem even if not marked as an explicit error.
111 | 5. Provide an overall assessment of the test run's health based *only* on the log content.
112 | 
113 | Return your analysis **strictly** in the following JSON format. Do not include any text outside of the JSON structure:
114 | 
115 | {
116 |   "overall_status": "Success|Failure|Warning",
117 |   "verified_steps": [ "Initialization", "PRD Parsing", /* ...other general steps observed... */ ],
118 |   "provider_add_task_comparison": {
119 |      "prompt_used": "... (extract from log if possible or state 'standard auth prompt') ...",
120 |      "provider_results": {
121 |        "anthropic": { "status": "Success|Failure|ID_Extraction_Failed|Set_Model_Failed", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },
122 |        "openai": { "status": "Success|Failure|...", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },
123 |        /* ... include all tested providers ... */
124 |      },
125 |      "comparison_summary": "Brief overall comparison of generated tasks..."
126 |    },
127 |   "detected_issues": [ { "severity": "Error|Warning|Anomaly", "description": "...", "log_context": "[Optional, short snippet from log near the issue]" } ],
128 |   "llm_summary_points": [ "Overall summary point 1", "Provider comparison highlight", "Any major issues noted" ]
129 | }
130 | 
131 | Here is the main log content:
132 | 
133 | %s
134 | EOF
135 | 
136 |   local full_prompt
137 |   if ! printf -v full_prompt "$prompt_template" "$log_content"; then
138 |     echo "[HELPER_ERROR] Failed to format prompt using printf." >&2
139 |     return 1
140 |   fi
141 | 
142 |   local payload
143 |   payload=$(jq -n --arg prompt "$full_prompt" '{
144 |     "model": "'"$llm_analysis_model_id"'",
145 |     "max_tokens": 3072,
146 |     "messages": [
147 |       {"role": "user", "content": $prompt}
148 |     ]
149 |   }') || {
150 |       echo "[HELPER_ERROR] Failed to create JSON payload using jq." >&2
151 |       return 1
152 |   }
153 | 
154 |   local response_raw response_http_code response_body
155 |   response_raw=$(curl -s -w "\nHTTP_STATUS_CODE:%{http_code}" -X POST "$api_endpoint" \
156 |        -H "Content-Type: application/json" \
157 |        -H "x-api-key: $api_key" \
158 |        -H "anthropic-version: 2023-06-01" \
159 |        --data "$payload")
160 | 
161 |   response_http_code=$(echo "$response_raw" | grep '^HTTP_STATUS_CODE:' | sed 's/HTTP_STATUS_CODE://')
162 |   response_body=$(echo "$response_raw" | sed '$d')
163 | 
164 |   if [ "$response_http_code" != "200" ]; then
165 |       echo "[HELPER_ERROR] LLM API call failed with HTTP status $response_http_code." >&2
166 |       echo "[HELPER_ERROR] Response Body: $response_body" >&2
167 |       return 1
168 |   fi
169 | 
170 |   if [ -z "$response_body" ]; then
171 |       echo "[HELPER_ERROR] LLM API call returned empty response body." >&2
172 |       return 1
173 |   fi
174 | 
175 |   # Calculate cost of this LLM analysis call
176 |   local input_tokens output_tokens input_cost_per_1m output_cost_per_1m calculated_llm_cost
177 |   input_tokens=$(echo "$response_body" | jq -r '.usage.input_tokens // 0')
178 |   output_tokens=$(echo "$response_body" | jq -r '.usage.output_tokens // 0')
179 | 
180 |   if [ -f "$supported_models_file" ]; then
181 |       model_cost_info=$(jq -r --arg provider "$llm_analysis_provider" --arg model_id "$llm_analysis_model_id" '
182 |           .[$provider][] | select(.id == $model_id) | .cost_per_1m_tokens
183 |       ' "$supported_models_file")
184 | 
185 |       if [[ -n "$model_cost_info" && "$model_cost_info" != "null" ]]; then
186 |           input_cost_per_1m=$(echo "$model_cost_info" | jq -r '.input // 0')
187 |           output_cost_per_1m=$(echo "$model_cost_info" | jq -r '.output // 0')
188 | 
189 |           calculated_llm_cost=$(echo "($input_tokens / 1000000 * $input_cost_per_1m) + ($output_tokens / 1000000 * $output_cost_per_1m)" | bc -l)
190 |           # Format to 6 decimal places
191 |           formatted_llm_cost=$(printf "%.6f" "$calculated_llm_cost")
192 |           echo "LLM Analysis AI Cost: $formatted_llm_cost USD" # This line will be parsed by run_e2e.sh
193 |       else
194 |           echo "[HELPER_WARNING] Cost data for model $llm_analysis_model_id not found in $supported_models_file. LLM analysis cost not calculated."
195 |       fi
196 |   else
197 |       echo "[HELPER_WARNING] $supported_models_file not found. LLM analysis cost not calculated."
198 |   fi
199 |   # --- End cost calculation for this call ---
200 | 
201 |   if echo "$response_body" | node "${project_root}/tests/e2e/parse_llm_output.cjs" "$log_file"; then
202 |       echo "[HELPER_SUCCESS] LLM analysis parsed and printed successfully by Node.js script."
203 |       return 0
204 |   else
205 |       local node_exit_code=$?
206 |       echo "[HELPER_ERROR] Node.js parsing script failed with exit code ${node_exit_code}."
207 |       echo "[HELPER_ERROR] Raw API response body (first 500 chars): $(echo "$response_body" | head -c 500)"
208 |       return 1
209 |   fi
210 | }
211 | 
212 | export -f analyze_log_with_llm 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/common/utils/git-utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Git utilities for Task Master
  3 |  * Git integration utilities using raw git commands and gh CLI
  4 |  */
  5 | 
  6 | import { exec, execSync } from 'child_process';
  7 | import { promisify } from 'util';
  8 | 
  9 | const execAsync = promisify(exec);
 10 | 
 11 | /**
 12 |  * GitHub repository information
 13 |  */
 14 | export interface GitHubRepoInfo {
 15 | 	name: string;
 16 | 	owner: { login: string };
 17 | 	defaultBranchRef: { name: string };
 18 | }
 19 | 
 20 | /**
 21 |  * Check if the specified directory is inside a git repository
 22 |  */
 23 | export async function isGitRepository(projectRoot: string): Promise<boolean> {
 24 | 	if (!projectRoot) {
 25 | 		throw new Error('projectRoot is required for isGitRepository');
 26 | 	}
 27 | 
 28 | 	try {
 29 | 		await execAsync('git rev-parse --git-dir', { cwd: projectRoot });
 30 | 		return true;
 31 | 	} catch (error) {
 32 | 		return false;
 33 | 	}
 34 | }
 35 | 
 36 | /**
 37 |  * Synchronous check if directory is in a git repository
 38 |  */
 39 | export function isGitRepositorySync(projectRoot: string): boolean {
 40 | 	if (!projectRoot) {
 41 | 		return false;
 42 | 	}
 43 | 
 44 | 	try {
 45 | 		execSync('git rev-parse --git-dir', {
 46 | 			cwd: projectRoot,
 47 | 			stdio: 'ignore'
 48 | 		});
 49 | 		return true;
 50 | 	} catch (error) {
 51 | 		return false;
 52 | 	}
 53 | }
 54 | 
 55 | /**
 56 |  * Get the current git branch name
 57 |  */
 58 | export async function getCurrentBranch(
 59 | 	projectRoot: string
 60 | ): Promise<string | null> {
 61 | 	if (!projectRoot) {
 62 | 		throw new Error('projectRoot is required for getCurrentBranch');
 63 | 	}
 64 | 
 65 | 	try {
 66 | 		const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
 67 | 			cwd: projectRoot
 68 | 		});
 69 | 		return stdout.trim();
 70 | 	} catch (error) {
 71 | 		return null;
 72 | 	}
 73 | }
 74 | 
 75 | /**
 76 |  * Synchronous get current git branch name
 77 |  */
 78 | export function getCurrentBranchSync(projectRoot: string): string | null {
 79 | 	if (!projectRoot) {
 80 | 		return null;
 81 | 	}
 82 | 
 83 | 	try {
 84 | 		const stdout = execSync('git rev-parse --abbrev-ref HEAD', {
 85 | 			cwd: projectRoot,
 86 | 			encoding: 'utf8'
 87 | 		});
 88 | 		return stdout.trim();
 89 | 	} catch (error) {
 90 | 		return null;
 91 | 	}
 92 | }
 93 | 
 94 | /**
 95 |  * Get list of all local git branches
 96 |  */
 97 | export async function getLocalBranches(projectRoot: string): Promise<string[]> {
 98 | 	if (!projectRoot) {
 99 | 		throw new Error('projectRoot is required for getLocalBranches');
100 | 	}
101 | 
102 | 	try {
103 | 		const { stdout } = await execAsync(
104 | 			'git branch --format="%(refname:short)"',
105 | 			{ cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 }
106 | 		);
107 | 		return stdout
108 | 			.trim()
109 | 			.split('\n')
110 | 			.filter((branch) => branch.length > 0)
111 | 			.map((branch) => branch.trim());
112 | 	} catch (error) {
113 | 		return [];
114 | 	}
115 | }
116 | 
117 | /**
118 |  * Get list of all remote branches
119 |  */
120 | export async function getRemoteBranches(
121 | 	projectRoot: string
122 | ): Promise<string[]> {
123 | 	if (!projectRoot) {
124 | 		throw new Error('projectRoot is required for getRemoteBranches');
125 | 	}
126 | 
127 | 	try {
128 | 		const { stdout } = await execAsync(
129 | 			'git branch -r --format="%(refname:short)"',
130 | 			{ cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 }
131 | 		);
132 | 		const names = stdout
133 | 			.trim()
134 | 			.split('\n')
135 | 			.filter((branch) => branch.length > 0 && !branch.includes('HEAD'))
136 | 			.map((branch) => branch.replace(/^[^/]+\//, '').trim());
137 | 		return Array.from(new Set(names));
138 | 	} catch (error) {
139 | 		return [];
140 | 	}
141 | }
142 | 
143 | /**
144 |  * Check if gh CLI is available and authenticated
145 |  */
146 | export async function isGhCliAvailable(projectRoot?: string): Promise<boolean> {
147 | 	try {
148 | 		const options = projectRoot ? { cwd: projectRoot } : {};
149 | 		await execAsync('gh auth status', options);
150 | 		return true;
151 | 	} catch (error) {
152 | 		return false;
153 | 	}
154 | }
155 | 
156 | /**
157 |  * Get GitHub repository information using gh CLI
158 |  */
159 | export async function getGitHubRepoInfo(
160 | 	projectRoot: string
161 | ): Promise<GitHubRepoInfo | null> {
162 | 	if (!projectRoot) {
163 | 		throw new Error('projectRoot is required for getGitHubRepoInfo');
164 | 	}
165 | 
166 | 	try {
167 | 		const { stdout } = await execAsync(
168 | 			'gh repo view --json name,owner,defaultBranchRef',
169 | 			{ cwd: projectRoot }
170 | 		);
171 | 		return JSON.parse(stdout) as GitHubRepoInfo;
172 | 	} catch (error) {
173 | 		return null;
174 | 	}
175 | }
176 | 
177 | /**
178 |  * Get git repository root directory
179 |  */
180 | export async function getGitRepositoryRoot(
181 | 	projectRoot: string
182 | ): Promise<string | null> {
183 | 	if (!projectRoot) {
184 | 		throw new Error('projectRoot is required for getGitRepositoryRoot');
185 | 	}
186 | 
187 | 	try {
188 | 		const { stdout } = await execAsync('git rev-parse --show-toplevel', {
189 | 			cwd: projectRoot
190 | 		});
191 | 		return stdout.trim();
192 | 	} catch (error) {
193 | 		return null;
194 | 	}
195 | }
196 | 
197 | /**
198 |  * Get the default branch name for the repository
199 |  */
200 | export async function getDefaultBranch(
201 | 	projectRoot: string
202 | ): Promise<string | null> {
203 | 	if (!projectRoot) {
204 | 		throw new Error('projectRoot is required for getDefaultBranch');
205 | 	}
206 | 
207 | 	try {
208 | 		// Try to get from GitHub first (if gh CLI is available)
209 | 		if (await isGhCliAvailable(projectRoot)) {
210 | 			const repoInfo = await getGitHubRepoInfo(projectRoot);
211 | 			if (repoInfo && repoInfo.defaultBranchRef) {
212 | 				return repoInfo.defaultBranchRef.name;
213 | 			}
214 | 		}
215 | 
216 | 		// Fallback to git remote info (support non-origin remotes)
217 | 		const remotesRaw = await execAsync('git remote', { cwd: projectRoot });
218 | 		const remotes = remotesRaw.stdout.trim().split('\n').filter(Boolean);
219 | 		if (remotes.length > 0) {
220 | 			const primary = remotes.includes('origin') ? 'origin' : remotes[0];
221 | 			// Parse `git remote show` (preferred)
222 | 			try {
223 | 				const { stdout } = await execAsync(`git remote show ${primary}`, {
224 | 					cwd: projectRoot,
225 | 					maxBuffer: 10 * 1024 * 1024
226 | 				});
227 | 				const m = stdout.match(/HEAD branch:\s+([^\s]+)/);
228 | 				if (m) return m[1].trim();
229 | 			} catch {}
230 | 			// Fallback to symbolic-ref of remote HEAD
231 | 			try {
232 | 				const { stdout } = await execAsync(
233 | 					`git symbolic-ref refs/remotes/${primary}/HEAD`,
234 | 					{ cwd: projectRoot }
235 | 				);
236 | 				return stdout.replace(`refs/remotes/${primary}/`, '').trim();
237 | 			} catch {}
238 | 		}
239 | 		// If we couldn't determine, throw to trigger final fallbacks
240 | 		throw new Error('default-branch-not-found');
241 | 	} catch (error) {
242 | 		// Final fallback - common default branch names
243 | 		const commonDefaults = ['main', 'master'];
244 | 		const branches = await getLocalBranches(projectRoot);
245 | 		const remoteBranches = await getRemoteBranches(projectRoot);
246 | 
247 | 		for (const defaultName of commonDefaults) {
248 | 			if (
249 | 				branches.includes(defaultName) ||
250 | 				remoteBranches.includes(defaultName)
251 | 			) {
252 | 				return defaultName;
253 | 			}
254 | 		}
255 | 
256 | 		return null;
257 | 	}
258 | }
259 | 
260 | /**
261 |  * Check if we're currently on the default branch
262 |  */
263 | export async function isOnDefaultBranch(projectRoot: string): Promise<boolean> {
264 | 	if (!projectRoot) {
265 | 		throw new Error('projectRoot is required for isOnDefaultBranch');
266 | 	}
267 | 
268 | 	try {
269 | 		const [currentBranch, defaultBranch] = await Promise.all([
270 | 			getCurrentBranch(projectRoot),
271 | 			getDefaultBranch(projectRoot)
272 | 		]);
273 | 		return (
274 | 			currentBranch !== null &&
275 | 			defaultBranch !== null &&
276 | 			currentBranch === defaultBranch
277 | 		);
278 | 	} catch (error) {
279 | 		return false;
280 | 	}
281 | }
282 | 
283 | /**
284 |  * Check if the current working directory is inside a Git work-tree
285 |  */
286 | export function insideGitWorkTree(): boolean {
287 | 	try {
288 | 		execSync('git rev-parse --is-inside-work-tree', {
289 | 			stdio: 'ignore',
290 | 			cwd: process.cwd()
291 | 		});
292 | 		return true;
293 | 	} catch {
294 | 		return false;
295 | 	}
296 | }
297 | 
298 | /**
299 |  * Sanitize branch name to be a valid tag name
300 |  */
301 | export function sanitizeBranchNameForTag(branchName: string): string {
302 | 	if (!branchName || typeof branchName !== 'string') {
303 | 		return 'unknown-branch';
304 | 	}
305 | 
306 | 	// Replace invalid characters with hyphens and clean up
307 | 	return branchName
308 | 		.replace(/[^a-zA-Z0-9_.-]/g, '-') // Replace invalid chars with hyphens (allow dots)
309 | 		.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
310 | 		.replace(/-+/g, '-') // Collapse multiple hyphens
311 | 		.toLowerCase() // Convert to lowercase
312 | 		.substring(0, 50); // Limit length
313 | }
314 | 
315 | /**
316 |  * Check if a branch name would create a valid tag name
317 |  */
318 | export function isValidBranchForTag(branchName: string): boolean {
319 | 	if (!branchName || typeof branchName !== 'string') {
320 | 		return false;
321 | 	}
322 | 
323 | 	// Check if it's a reserved branch name that shouldn't become tags
324 | 	const reservedBranches = ['main', 'master', 'develop', 'dev', 'head'];
325 | 	if (reservedBranches.includes(branchName.toLowerCase())) {
326 | 		return false;
327 | 	}
328 | 
329 | 	// Check if sanitized name would be meaningful
330 | 	const sanitized = sanitizeBranchNameForTag(branchName);
331 | 	return sanitized.length > 0 && sanitized !== 'unknown-branch';
332 | }
333 | 
334 | /**
335 |  * Git worktree information
336 |  */
337 | export interface GitWorktree {
338 | 	path: string;
339 | 	branch: string | null;
340 | 	head: string;
341 | }
342 | 
343 | /**
344 |  * Get list of all git worktrees
345 |  */
346 | export async function getWorktrees(
347 | 	projectRoot: string
348 | ): Promise<GitWorktree[]> {
349 | 	if (!projectRoot) {
350 | 		throw new Error('projectRoot is required for getWorktrees');
351 | 	}
352 | 
353 | 	try {
354 | 		const { stdout } = await execAsync('git worktree list --porcelain', {
355 | 			cwd: projectRoot
356 | 		});
357 | 
358 | 		const worktrees: GitWorktree[] = [];
359 | 		const lines = stdout.trim().split('\n');
360 | 		let current: Partial<GitWorktree> = {};
361 | 
362 | 		for (const line of lines) {
363 | 			if (line.startsWith('worktree ')) {
364 | 				// flush previous entry if present
365 | 				if (current.path) {
366 | 					worktrees.push({
367 | 						path: current.path,
368 | 						branch: current.branch || null,
369 | 						head: current.head || ''
370 | 					});
371 | 					current = {};
372 | 				}
373 | 				current.path = line.substring(9);
374 | 			} else if (line.startsWith('HEAD ')) {
375 | 				current.head = line.substring(5);
376 | 			} else if (line.startsWith('branch ')) {
377 | 				current.branch = line.substring(7).replace('refs/heads/', '');
378 | 			} else if (line === '' && current.path) {
379 | 				worktrees.push({
380 | 					path: current.path,
381 | 					branch: current.branch || null,
382 | 					head: current.head || ''
383 | 				});
384 | 				current = {};
385 | 			}
386 | 		}
387 | 
388 | 		// Handle last entry if no trailing newline
389 | 		if (current.path) {
390 | 			worktrees.push({
391 | 				path: current.path,
392 | 				branch: current.branch || null,
393 | 				head: current.head || ''
394 | 			});
395 | 		}
396 | 
397 | 		return worktrees;
398 | 	} catch (error) {
399 | 		return [];
400 | 	}
401 | }
402 | 
403 | /**
404 |  * Check if a branch is checked out in any worktree
405 |  * Returns the worktree path if found, null otherwise
406 |  */
407 | export async function isBranchCheckedOut(
408 | 	projectRoot: string,
409 | 	branchName: string
410 | ): Promise<string | null> {
411 | 	if (!projectRoot) {
412 | 		throw new Error('projectRoot is required for isBranchCheckedOut');
413 | 	}
414 | 	if (!branchName) {
415 | 		throw new Error('branchName is required for isBranchCheckedOut');
416 | 	}
417 | 
418 | 	const worktrees = await getWorktrees(projectRoot);
419 | 	const worktree = worktrees.find((wt) => wt.branch === branchName);
420 | 	return worktree ? worktree.path : null;
421 | }
422 | 
```

--------------------------------------------------------------------------------
/scripts/modules/task-manager/parse-prd/parse-prd-helpers.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Helper functions for PRD parsing
  3 |  */
  4 | 
  5 | import fs from 'fs';
  6 | import path from 'path';
  7 | import boxen from 'boxen';
  8 | import chalk from 'chalk';
  9 | import { ensureTagMetadata, findTaskById } from '../../utils.js';
 10 | import { displayParsePrdSummary } from '../../../../src/ui/parse-prd.js';
 11 | import { TimeoutManager } from '../../../../src/utils/timeout-manager.js';
 12 | import { displayAiUsageSummary } from '../../ui.js';
 13 | import { getPromptManager } from '../../prompt-manager.js';
 14 | import { getDefaultPriority } from '../../config-manager.js';
 15 | 
 16 | /**
 17 |  * Estimate token count from text
 18 |  * @param {string} text - Text to estimate tokens for
 19 |  * @returns {number} Estimated token count
 20 |  */
 21 | export function estimateTokens(text) {
 22 | 	// Common approximation: ~4 characters per token for English
 23 | 	return Math.ceil(text.length / 4);
 24 | }
 25 | 
 26 | /**
 27 |  * Read and validate PRD content
 28 |  * @param {string} prdPath - Path to PRD file
 29 |  * @returns {string} PRD content
 30 |  * @throws {Error} If file is empty or cannot be read
 31 |  */
 32 | export function readPrdContent(prdPath) {
 33 | 	const prdContent = fs.readFileSync(prdPath, 'utf8');
 34 | 	if (!prdContent) {
 35 | 		throw new Error(`Input file ${prdPath} is empty or could not be read.`);
 36 | 	}
 37 | 	return prdContent;
 38 | }
 39 | 
 40 | /**
 41 |  * Load existing tasks from file
 42 |  * @param {string} tasksPath - Path to tasks file
 43 |  * @param {string} targetTag - Target tag to load from
 44 |  * @returns {{tasks: Array, nextId: number}} Existing tasks and next ID
 45 |  */
 46 | export function loadExistingTasks(tasksPath, targetTag) {
 47 | 	let existingTasks = [];
 48 | 	let nextId = 1;
 49 | 
 50 | 	if (!fs.existsSync(tasksPath)) {
 51 | 		return { existingTasks, nextId };
 52 | 	}
 53 | 
 54 | 	try {
 55 | 		const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
 56 | 		const allData = JSON.parse(existingFileContent);
 57 | 
 58 | 		if (allData[targetTag]?.tasks && Array.isArray(allData[targetTag].tasks)) {
 59 | 			existingTasks = allData[targetTag].tasks;
 60 | 			if (existingTasks.length > 0) {
 61 | 				nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
 62 | 			}
 63 | 		}
 64 | 	} catch (error) {
 65 | 		// If we can't read the file or parse it, assume no existing tasks
 66 | 		return { existingTasks: [], nextId: 1 };
 67 | 	}
 68 | 
 69 | 	return { existingTasks, nextId };
 70 | }
 71 | 
 72 | /**
 73 |  * Validate overwrite/append operations
 74 |  * @param {Object} params
 75 |  * @returns {void}
 76 |  * @throws {Error} If validation fails
 77 |  */
 78 | export function validateFileOperations({
 79 | 	existingTasks,
 80 | 	targetTag,
 81 | 	append,
 82 | 	force,
 83 | 	isMCP,
 84 | 	logger
 85 | }) {
 86 | 	const hasExistingTasks = existingTasks.length > 0;
 87 | 
 88 | 	if (!hasExistingTasks) {
 89 | 		logger.report(
 90 | 			`Tag '${targetTag}' is empty or doesn't exist. Creating/updating tag with new tasks.`,
 91 | 			'info'
 92 | 		);
 93 | 		return;
 94 | 	}
 95 | 
 96 | 	if (append) {
 97 | 		logger.report(
 98 | 			`Append mode enabled. Found ${existingTasks.length} existing tasks in tag '${targetTag}'.`,
 99 | 			'info'
100 | 		);
101 | 		return;
102 | 	}
103 | 
104 | 	if (!force) {
105 | 		const errorMessage = `Tag '${targetTag}' already contains ${existingTasks.length} tasks. Use --force to overwrite or --append to add to existing tasks.`;
106 | 		logger.report(errorMessage, 'error');
107 | 
108 | 		if (isMCP) {
109 | 			throw new Error(errorMessage);
110 | 		} else {
111 | 			console.error(chalk.red(errorMessage));
112 | 			process.exit(1);
113 | 		}
114 | 	}
115 | 
116 | 	logger.report(
117 | 		`Force flag enabled. Overwriting existing tasks in tag '${targetTag}'.`,
118 | 		'debug'
119 | 	);
120 | }
121 | 
122 | /**
123 |  * Process and transform tasks with ID remapping
124 |  * @param {Array} rawTasks - Raw tasks from AI
125 |  * @param {number} startId - Starting ID for new tasks
126 |  * @param {Array} existingTasks - Existing tasks for dependency validation
127 |  * @param {string} defaultPriority - Default priority for tasks
128 |  * @returns {Array} Processed tasks with remapped IDs
129 |  */
130 | export function processTasks(
131 | 	rawTasks,
132 | 	startId,
133 | 	existingTasks,
134 | 	defaultPriority
135 | ) {
136 | 	let currentId = startId;
137 | 	const taskMap = new Map();
138 | 
139 | 	// First pass: assign new IDs and create mapping
140 | 	const processedTasks = rawTasks.map((task) => {
141 | 		const newId = currentId++;
142 | 		taskMap.set(task.id, newId);
143 | 
144 | 		return {
145 | 			...task,
146 | 			id: newId,
147 | 			status: task.status || 'pending',
148 | 			priority: task.priority || defaultPriority,
149 | 			dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
150 | 			subtasks: task.subtasks || [],
151 | 			// Ensure all required fields have values
152 | 			title: task.title || '',
153 | 			description: task.description || '',
154 | 			details: task.details || '',
155 | 			testStrategy: task.testStrategy || ''
156 | 		};
157 | 	});
158 | 
159 | 	// Second pass: remap dependencies
160 | 	processedTasks.forEach((task) => {
161 | 		task.dependencies = task.dependencies
162 | 			.map((depId) => taskMap.get(depId))
163 | 			.filter(
164 | 				(newDepId) =>
165 | 					newDepId != null &&
166 | 					newDepId < task.id &&
167 | 					(findTaskById(existingTasks, newDepId) ||
168 | 						processedTasks.some((t) => t.id === newDepId))
169 | 			);
170 | 	});
171 | 
172 | 	return processedTasks;
173 | }
174 | 
175 | /**
176 |  * Save tasks to file with tag support
177 |  * @param {string} tasksPath - Path to save tasks
178 |  * @param {Array} tasks - Tasks to save
179 |  * @param {string} targetTag - Target tag
180 |  * @param {Object} logger - Logger instance
181 |  */
182 | export function saveTasksToFile(tasksPath, tasks, targetTag, logger) {
183 | 	// Create directory if it doesn't exist
184 | 	const tasksDir = path.dirname(tasksPath);
185 | 	if (!fs.existsSync(tasksDir)) {
186 | 		fs.mkdirSync(tasksDir, { recursive: true });
187 | 	}
188 | 
189 | 	// Read existing file to preserve other tags
190 | 	let outputData = {};
191 | 	if (fs.existsSync(tasksPath)) {
192 | 		try {
193 | 			const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
194 | 			outputData = JSON.parse(existingFileContent);
195 | 		} catch (error) {
196 | 			outputData = {};
197 | 		}
198 | 	}
199 | 
200 | 	// Update only the target tag
201 | 	outputData[targetTag] = {
202 | 		tasks: tasks,
203 | 		metadata: {
204 | 			created:
205 | 				outputData[targetTag]?.metadata?.created || new Date().toISOString(),
206 | 			updated: new Date().toISOString(),
207 | 			description: `Tasks for ${targetTag} context`
208 | 		}
209 | 	};
210 | 
211 | 	// Ensure proper metadata
212 | 	ensureTagMetadata(outputData[targetTag], {
213 | 		description: `Tasks for ${targetTag} context`
214 | 	});
215 | 
216 | 	// Write back to file
217 | 	fs.writeFileSync(tasksPath, JSON.stringify(outputData, null, 2));
218 | 
219 | 	logger.report(
220 | 		`Successfully saved ${tasks.length} tasks to ${tasksPath}`,
221 | 		'debug'
222 | 	);
223 | }
224 | 
225 | /**
226 |  * Build prompts for AI service
227 |  * @param {Object} config - Configuration object
228 |  * @param {string} prdContent - PRD content
229 |  * @param {number} nextId - Next task ID
230 |  * @returns {Promise<{systemPrompt: string, userPrompt: string}>}
231 |  */
232 | export async function buildPrompts(config, prdContent, nextId) {
233 | 	const promptManager = getPromptManager();
234 | 	const defaultTaskPriority =
235 | 		getDefaultPriority(config.projectRoot) || 'medium';
236 | 
237 | 	return promptManager.loadPrompt('parse-prd', {
238 | 		research: config.research,
239 | 		numTasks: config.numTasks,
240 | 		nextId,
241 | 		prdContent,
242 | 		prdPath: config.prdPath,
243 | 		defaultTaskPriority,
244 | 		hasCodebaseAnalysis: config.hasCodebaseAnalysis(),
245 | 		projectRoot: config.projectRoot || ''
246 | 	});
247 | }
248 | 
249 | /**
250 |  * Handle progress reporting for both CLI and MCP
251 |  * @param {Object} params
252 |  */
253 | export async function reportTaskProgress({
254 | 	task,
255 | 	currentCount,
256 | 	totalTasks,
257 | 	estimatedTokens,
258 | 	progressTracker,
259 | 	reportProgress,
260 | 	priorityMap,
261 | 	defaultPriority,
262 | 	estimatedInputTokens
263 | }) {
264 | 	const priority = task.priority || defaultPriority;
265 | 	const priorityIndicator = priorityMap[priority] || priorityMap.medium;
266 | 
267 | 	// CLI progress tracker
268 | 	if (progressTracker) {
269 | 		progressTracker.addTaskLine(currentCount, task.title, priority);
270 | 		if (estimatedTokens) {
271 | 			progressTracker.updateTokens(estimatedInputTokens, estimatedTokens);
272 | 		}
273 | 	}
274 | 
275 | 	// MCP progress reporting
276 | 	if (reportProgress) {
277 | 		try {
278 | 			const outputTokens = estimatedTokens
279 | 				? Math.floor(estimatedTokens / totalTasks)
280 | 				: 0;
281 | 
282 | 			await reportProgress({
283 | 				progress: currentCount,
284 | 				total: totalTasks,
285 | 				message: `${priorityIndicator} Task ${currentCount}/${totalTasks} - ${task.title} | ~Output: ${outputTokens} tokens`
286 | 			});
287 | 		} catch (error) {
288 | 			// Ignore progress reporting errors
289 | 		}
290 | 	}
291 | }
292 | 
293 | /**
294 |  * Display completion summary for CLI
295 |  * @param {Object} params
296 |  */
297 | export async function displayCliSummary({
298 | 	processedTasks,
299 | 	nextId,
300 | 	summary,
301 | 	prdPath,
302 | 	tasksPath,
303 | 	usedFallback,
304 | 	aiServiceResponse
305 | }) {
306 | 	// Generate task file names
307 | 	const taskFilesGenerated = (() => {
308 | 		if (!Array.isArray(processedTasks) || processedTasks.length === 0) {
309 | 			return `task_${String(nextId).padStart(3, '0')}.txt`;
310 | 		}
311 | 		const firstNewTaskId = processedTasks[0].id;
312 | 		const lastNewTaskId = processedTasks[processedTasks.length - 1].id;
313 | 		if (processedTasks.length === 1) {
314 | 			return `task_${String(firstNewTaskId).padStart(3, '0')}.txt`;
315 | 		}
316 | 		return `task_${String(firstNewTaskId).padStart(3, '0')}.txt -> task_${String(lastNewTaskId).padStart(3, '0')}.txt`;
317 | 	})();
318 | 
319 | 	displayParsePrdSummary({
320 | 		totalTasks: processedTasks.length,
321 | 		taskPriorities: summary.taskPriorities,
322 | 		prdFilePath: prdPath,
323 | 		outputPath: tasksPath,
324 | 		elapsedTime: summary.elapsedTime,
325 | 		usedFallback,
326 | 		taskFilesGenerated,
327 | 		actionVerb: summary.actionVerb
328 | 	});
329 | 
330 | 	// Display telemetry
331 | 	if (aiServiceResponse?.telemetryData) {
332 | 		// For streaming, wait briefly to allow usage data to be captured
333 | 		if (aiServiceResponse.mainResult?.usage) {
334 | 			// Give the usage promise a short time to resolve
335 | 			await TimeoutManager.withSoftTimeout(
336 | 				aiServiceResponse.mainResult.usage,
337 | 				1000,
338 | 				undefined
339 | 			);
340 | 		}
341 | 		displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
342 | 	}
343 | }
344 | 
345 | /**
346 |  * Display non-streaming CLI output
347 |  * @param {Object} params
348 |  */
349 | export function displayNonStreamingCliOutput({
350 | 	processedTasks,
351 | 	research,
352 | 	finalTasks,
353 | 	tasksPath,
354 | 	aiServiceResponse
355 | }) {
356 | 	console.log(
357 | 		boxen(
358 | 			chalk.green(
359 | 				`Successfully generated ${processedTasks.length} new tasks${research ? ' with research-backed analysis' : ''}. Total tasks in ${tasksPath}: ${finalTasks.length}`
360 | 			),
361 | 			{ padding: 1, borderColor: 'green', borderStyle: 'round' }
362 | 		)
363 | 	);
364 | 
365 | 	console.log(
366 | 		boxen(
367 | 			chalk.white.bold('Next Steps:') +
368 | 				'\n\n' +
369 | 				`${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
370 | 				`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
371 | 			{
372 | 				padding: 1,
373 | 				borderColor: 'cyan',
374 | 				borderStyle: 'round',
375 | 				margin: { top: 1 }
376 | 			}
377 | 		)
378 | 	);
379 | 
380 | 	if (aiServiceResponse?.telemetryData) {
381 | 		displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
382 | 	}
383 | }
384 | 
```

--------------------------------------------------------------------------------
/tests/unit/mcp/tools/initialize-project.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Tests for the initialize-project MCP tool
  3 |  *
  4 |  * Note: This test does NOT test the actual implementation. It tests that:
  5 |  * 1. The tool is registered correctly with the correct parameters
  6 |  * 2. Command construction works correctly with various arguments
  7 |  * 3. Error handling works as expected
  8 |  * 4. Response formatting is correct
  9 |  *
 10 |  * We do NOT import the real implementation - everything is mocked
 11 |  */
 12 | 
 13 | import { jest } from '@jest/globals';
 14 | 
 15 | // Mock child_process.execSync
 16 | const mockExecSync = jest.fn();
 17 | jest.mock('child_process', () => ({
 18 | 	execSync: mockExecSync
 19 | }));
 20 | 
 21 | // Mock the utility functions
 22 | const mockCreateContentResponse = jest.fn((content) => ({
 23 | 	content
 24 | }));
 25 | 
 26 | const mockCreateErrorResponse = jest.fn((message, details) => ({
 27 | 	error: { message, details }
 28 | }));
 29 | 
 30 | jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
 31 | 	createContentResponse: mockCreateContentResponse,
 32 | 	createErrorResponse: mockCreateErrorResponse
 33 | }));
 34 | 
 35 | // Mock the z object from zod
 36 | const mockZod = {
 37 | 	object: jest.fn(() => mockZod),
 38 | 	string: jest.fn(() => mockZod),
 39 | 	boolean: jest.fn(() => mockZod),
 40 | 	optional: jest.fn(() => mockZod),
 41 | 	default: jest.fn(() => mockZod),
 42 | 	describe: jest.fn(() => mockZod),
 43 | 	_def: {
 44 | 		shape: () => ({
 45 | 			projectName: {},
 46 | 			projectDescription: {},
 47 | 			projectVersion: {},
 48 | 			authorName: {},
 49 | 			skipInstall: {},
 50 | 			addAliases: {},
 51 | 			yes: {}
 52 | 		})
 53 | 	}
 54 | };
 55 | 
 56 | jest.mock('zod', () => ({
 57 | 	z: mockZod
 58 | }));
 59 | 
 60 | // Create our own simplified version of the registerInitializeProjectTool function
 61 | const registerInitializeProjectTool = (server) => {
 62 | 	server.addTool({
 63 | 		name: 'initialize_project',
 64 | 		description:
 65 | 			"Initializes a new Task Master project structure in the current working directory by running 'task-master init'.",
 66 | 		parameters: mockZod,
 67 | 		execute: async (args, { log }) => {
 68 | 			try {
 69 | 				log.info(
 70 | 					`Executing initialize_project with args: ${JSON.stringify(args)}`
 71 | 				);
 72 | 
 73 | 				// Construct the command arguments
 74 | 				let command = 'npx task-master init';
 75 | 				const cliArgs = [];
 76 | 				if (args.projectName) {
 77 | 					cliArgs.push(`--name "${args.projectName.replace(/"/g, '\\"')}"`);
 78 | 				}
 79 | 				if (args.projectDescription) {
 80 | 					cliArgs.push(
 81 | 						`--description "${args.projectDescription.replace(/"/g, '\\"')}"`
 82 | 					);
 83 | 				}
 84 | 				if (args.projectVersion) {
 85 | 					cliArgs.push(
 86 | 						`--version "${args.projectVersion.replace(/"/g, '\\"')}"`
 87 | 					);
 88 | 				}
 89 | 				if (args.authorName) {
 90 | 					cliArgs.push(`--author "${args.authorName.replace(/"/g, '\\"')}"`);
 91 | 				}
 92 | 				if (args.skipInstall) cliArgs.push('--skip-install');
 93 | 				if (args.addAliases) cliArgs.push('--aliases');
 94 | 				if (args.yes) cliArgs.push('--yes');
 95 | 
 96 | 				command += ' ' + cliArgs.join(' ');
 97 | 
 98 | 				log.info(`Constructed command: ${command}`);
 99 | 
100 | 				// Execute the command
101 | 				const output = mockExecSync(command, {
102 | 					encoding: 'utf8',
103 | 					stdio: 'pipe',
104 | 					timeout: 300000
105 | 				});
106 | 
107 | 				log.info(`Initialization output:\n${output}`);
108 | 
109 | 				// Return success response
110 | 				return mockCreateContentResponse({
111 | 					message: 'Project initialized successfully.',
112 | 					next_step:
113 | 						'Now that the project is initialized, the next step is to create the tasks by parsing a PRD. This will create the tasks folder and the initial task files. The parse-prd tool will required a PRD file',
114 | 					output: output
115 | 				});
116 | 			} catch (error) {
117 | 				// Catch errors
118 | 				const errorMessage = `Project initialization failed: ${error.message}`;
119 | 				const errorDetails =
120 | 					error.stderr?.toString() || error.stdout?.toString() || error.message;
121 | 				log.error(`${errorMessage}\nDetails: ${errorDetails}`);
122 | 
123 | 				// Return error response
124 | 				return mockCreateErrorResponse(errorMessage, { details: errorDetails });
125 | 			}
126 | 		}
127 | 	});
128 | };
129 | 
130 | describe('Initialize Project MCP Tool', () => {
131 | 	// Mock server and logger
132 | 	let mockServer;
133 | 	let executeFunction;
134 | 
135 | 	const mockLogger = {
136 | 		debug: jest.fn(),
137 | 		info: jest.fn(),
138 | 		warn: jest.fn(),
139 | 		error: jest.fn()
140 | 	};
141 | 
142 | 	beforeEach(() => {
143 | 		// Clear all mocks before each test
144 | 		jest.clearAllMocks();
145 | 
146 | 		// Create mock server
147 | 		mockServer = {
148 | 			addTool: jest.fn((config) => {
149 | 				executeFunction = config.execute;
150 | 			})
151 | 		};
152 | 
153 | 		// Default mock behavior
154 | 		mockExecSync.mockReturnValue('Project initialized successfully.');
155 | 
156 | 		// Register the tool to capture the tool definition
157 | 		registerInitializeProjectTool(mockServer);
158 | 	});
159 | 
160 | 	test('registers the tool with correct name and parameters', () => {
161 | 		// Check that addTool was called
162 | 		expect(mockServer.addTool).toHaveBeenCalledTimes(1);
163 | 
164 | 		// Extract the tool definition from the mock call
165 | 		const toolDefinition = mockServer.addTool.mock.calls[0][0];
166 | 
167 | 		// Verify tool properties
168 | 		expect(toolDefinition.name).toBe('initialize_project');
169 | 		expect(toolDefinition.description).toContain(
170 | 			'Initializes a new Task Master project'
171 | 		);
172 | 		expect(toolDefinition).toHaveProperty('parameters');
173 | 		expect(toolDefinition).toHaveProperty('execute');
174 | 	});
175 | 
176 | 	test('constructs command with proper arguments', async () => {
177 | 		// Create arguments with all parameters
178 | 		const args = {
179 | 			projectName: 'Test Project',
180 | 			projectDescription: 'A project for testing',
181 | 			projectVersion: '1.0.0',
182 | 			authorName: 'Test Author',
183 | 			skipInstall: true,
184 | 			addAliases: true,
185 | 			yes: true
186 | 		};
187 | 
188 | 		// Execute the tool
189 | 		await executeFunction(args, { log: mockLogger });
190 | 
191 | 		// Verify execSync was called with the expected command
192 | 		expect(mockExecSync).toHaveBeenCalledTimes(1);
193 | 
194 | 		const command = mockExecSync.mock.calls[0][0];
195 | 
196 | 		// Check that the command includes npx task-master init
197 | 		expect(command).toContain('npx task-master init');
198 | 
199 | 		// Verify each argument is correctly formatted in the command
200 | 		expect(command).toContain('--name "Test Project"');
201 | 		expect(command).toContain('--description "A project for testing"');
202 | 		expect(command).toContain('--version "1.0.0"');
203 | 		expect(command).toContain('--author "Test Author"');
204 | 		expect(command).toContain('--skip-install');
205 | 		expect(command).toContain('--aliases');
206 | 		expect(command).toContain('--yes');
207 | 	});
208 | 
209 | 	test('properly escapes special characters in arguments', async () => {
210 | 		// Create arguments with special characters
211 | 		const args = {
212 | 			projectName: 'Test "Quoted" Project',
213 | 			projectDescription: 'A "special" project for testing'
214 | 		};
215 | 
216 | 		// Execute the tool
217 | 		await executeFunction(args, { log: mockLogger });
218 | 
219 | 		// Get the command that was executed
220 | 		const command = mockExecSync.mock.calls[0][0];
221 | 
222 | 		// Verify quotes were properly escaped
223 | 		expect(command).toContain('--name "Test \\"Quoted\\" Project"');
224 | 		expect(command).toContain(
225 | 			'--description "A \\"special\\" project for testing"'
226 | 		);
227 | 	});
228 | 
229 | 	test('returns success response when command succeeds', async () => {
230 | 		// Set up the mock to return specific output
231 | 		const outputMessage = 'Project initialized successfully.';
232 | 		mockExecSync.mockReturnValueOnce(outputMessage);
233 | 
234 | 		// Execute the tool
235 | 		const result = await executeFunction({}, { log: mockLogger });
236 | 
237 | 		// Verify createContentResponse was called with the right arguments
238 | 		expect(mockCreateContentResponse).toHaveBeenCalledWith(
239 | 			expect.objectContaining({
240 | 				message: 'Project initialized successfully.',
241 | 				next_step: expect.any(String),
242 | 				output: outputMessage
243 | 			})
244 | 		);
245 | 
246 | 		// Verify the returned result has the expected structure
247 | 		expect(result).toHaveProperty('content');
248 | 		expect(result.content).toHaveProperty('message');
249 | 		expect(result.content).toHaveProperty('next_step');
250 | 		expect(result.content).toHaveProperty('output');
251 | 		expect(result.content.output).toBe(outputMessage);
252 | 	});
253 | 
254 | 	test('returns error response when command fails', async () => {
255 | 		// Create an error to be thrown
256 | 		const error = new Error('Command failed');
257 | 		error.stdout = 'Some standard output';
258 | 		error.stderr = 'Some error output';
259 | 
260 | 		// Make the mock throw the error
261 | 		mockExecSync.mockImplementationOnce(() => {
262 | 			throw error;
263 | 		});
264 | 
265 | 		// Execute the tool
266 | 		const result = await executeFunction({}, { log: mockLogger });
267 | 
268 | 		// Verify createErrorResponse was called with the right arguments
269 | 		expect(mockCreateErrorResponse).toHaveBeenCalledWith(
270 | 			'Project initialization failed: Command failed',
271 | 			expect.objectContaining({
272 | 				details: 'Some error output'
273 | 			})
274 | 		);
275 | 
276 | 		// Verify the returned result has the expected structure
277 | 		expect(result).toHaveProperty('error');
278 | 		expect(result.error).toHaveProperty('message');
279 | 		expect(result.error.message).toContain('Project initialization failed');
280 | 	});
281 | 
282 | 	test('logs information about the execution', async () => {
283 | 		// Execute the tool
284 | 		await executeFunction({}, { log: mockLogger });
285 | 
286 | 		// Verify that logging occurred
287 | 		expect(mockLogger.info).toHaveBeenCalledWith(
288 | 			expect.stringContaining('Executing initialize_project')
289 | 		);
290 | 		expect(mockLogger.info).toHaveBeenCalledWith(
291 | 			expect.stringContaining('Constructed command')
292 | 		);
293 | 		expect(mockLogger.info).toHaveBeenCalledWith(
294 | 			expect.stringContaining('Initialization output')
295 | 		);
296 | 	});
297 | 
298 | 	test('uses fallback to stdout if stderr is not available in error', async () => {
299 | 		// Create an error with only stdout
300 | 		const error = new Error('Command failed');
301 | 		error.stdout = 'Some standard output with error details';
302 | 		// No stderr property
303 | 
304 | 		// Make the mock throw the error
305 | 		mockExecSync.mockImplementationOnce(() => {
306 | 			throw error;
307 | 		});
308 | 
309 | 		// Execute the tool
310 | 		await executeFunction({}, { log: mockLogger });
311 | 
312 | 		// Verify createErrorResponse was called with stdout as details
313 | 		expect(mockCreateErrorResponse).toHaveBeenCalledWith(
314 | 			expect.any(String),
315 | 			expect.objectContaining({
316 | 				details: 'Some standard output with error details'
317 | 			})
318 | 		);
319 | 	});
320 | 
321 | 	test('logs error details when command fails', async () => {
322 | 		// Create an error
323 | 		const error = new Error('Command failed');
324 | 		error.stderr = 'Some detailed error message';
325 | 
326 | 		// Make the mock throw the error
327 | 		mockExecSync.mockImplementationOnce(() => {
328 | 			throw error;
329 | 		});
330 | 
331 | 		// Execute the tool
332 | 		await executeFunction({}, { log: mockLogger });
333 | 
334 | 		// Verify error logging
335 | 		expect(mockLogger.error).toHaveBeenCalledWith(
336 | 			expect.stringContaining('Project initialization failed')
337 | 		);
338 | 		expect(mockLogger.error).toHaveBeenCalledWith(
339 | 			expect.stringContaining('Some detailed error message')
340 | 		);
341 | 	});
342 | });
343 | 
```

--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer-kiro.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | 
  3 | // Mock fs module before importing anything that uses it
  4 | jest.mock('fs', () => ({
  5 | 	readFileSync: jest.fn(),
  6 | 	writeFileSync: jest.fn(),
  7 | 	existsSync: jest.fn(),
  8 | 	mkdirSync: jest.fn(),
  9 | 	readdirSync: jest.fn(),
 10 | 	copyFileSync: jest.fn()
 11 | }));
 12 | 
 13 | // Mock the log function
 14 | jest.mock('../../../scripts/modules/utils.js', () => ({
 15 | 	log: jest.fn(),
 16 | 	isSilentMode: jest.fn().mockReturnValue(false)
 17 | }));
 18 | 
 19 | // Import modules after mocking
 20 | import fs from 'fs';
 21 | import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
 22 | import { kiroProfile } from '../../../src/profiles/kiro.js';
 23 | 
 24 | describe('Kiro Rule Transformer', () => {
 25 | 	// Set up spies on the mocked modules
 26 | 	const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
 27 | 	const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
 28 | 	const mockExistsSync = jest.spyOn(fs, 'existsSync');
 29 | 	const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
 30 | 	const mockConsoleError = jest
 31 | 		.spyOn(console, 'error')
 32 | 		.mockImplementation(() => {});
 33 | 	jest.spyOn(console, 'log').mockImplementation(() => {});
 34 | 
 35 | 	beforeEach(() => {
 36 | 		jest.clearAllMocks();
 37 | 		// Setup default mocks
 38 | 		mockReadFileSync.mockReturnValue('');
 39 | 		mockWriteFileSync.mockImplementation(() => {});
 40 | 		mockExistsSync.mockReturnValue(true);
 41 | 		mockMkdirSync.mockImplementation(() => {});
 42 | 	});
 43 | 
 44 | 	afterAll(() => {
 45 | 		jest.restoreAllMocks();
 46 | 	});
 47 | 
 48 | 	it('should correctly convert basic terms', () => {
 49 | 		const testContent = `---
 50 | description: Test Cursor rule for basic terms
 51 | globs: **/*
 52 | alwaysApply: true
 53 | ---
 54 | 
 55 | This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
 56 | Also has references to .mdc files.`;
 57 | 
 58 | 		// Mock file read to return our test content
 59 | 		mockReadFileSync.mockReturnValue(testContent);
 60 | 
 61 | 		// Mock file system operations
 62 | 		mockExistsSync.mockReturnValue(true);
 63 | 
 64 | 		// Call the function
 65 | 		const result = convertRuleToProfileRule(
 66 | 			'test-source.mdc',
 67 | 			'test-target.md',
 68 | 			kiroProfile
 69 | 		);
 70 | 
 71 | 		// Verify the result
 72 | 		expect(result).toBe(true);
 73 | 		expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
 74 | 
 75 | 		// Get the transformed content
 76 | 		const transformedContent = mockWriteFileSync.mock.calls[0][1];
 77 | 
 78 | 		// Verify Cursor -> Kiro transformations
 79 | 		expect(transformedContent).toContain('kiro.dev');
 80 | 		expect(transformedContent).toContain('Kiro');
 81 | 		expect(transformedContent).not.toContain('cursor.so');
 82 | 		expect(transformedContent).not.toContain('Cursor');
 83 | 		expect(transformedContent).toContain('.md');
 84 | 		expect(transformedContent).not.toContain('.mdc');
 85 | 	});
 86 | 
 87 | 	it('should handle URL transformations', () => {
 88 | 		const testContent = `Visit https://cursor.so/docs for more information.
 89 | Also check out cursor.so and www.cursor.so for updates.`;
 90 | 
 91 | 		mockReadFileSync.mockReturnValue(testContent);
 92 | 		mockExistsSync.mockReturnValue(true);
 93 | 
 94 | 		const result = convertRuleToProfileRule(
 95 | 			'test-source.mdc',
 96 | 			'test-target.md',
 97 | 			kiroProfile
 98 | 		);
 99 | 
100 | 		expect(result).toBe(true);
101 | 		const transformedContent = mockWriteFileSync.mock.calls[0][1];
102 | 
103 | 		// Verify URL transformations
104 | 		expect(transformedContent).toContain('https://kiro.dev');
105 | 		expect(transformedContent).toContain('kiro.dev');
106 | 		expect(transformedContent).not.toContain('cursor.so');
107 | 	});
108 | 
109 | 	it('should handle file extension transformations', () => {
110 | 		const testContent = `This rule references file.mdc and another.mdc file.
111 | Use the .mdc extension for all rule files.`;
112 | 
113 | 		mockReadFileSync.mockReturnValue(testContent);
114 | 		mockExistsSync.mockReturnValue(true);
115 | 
116 | 		const result = convertRuleToProfileRule(
117 | 			'test-source.mdc',
118 | 			'test-target.md',
119 | 			kiroProfile
120 | 		);
121 | 
122 | 		expect(result).toBe(true);
123 | 		const transformedContent = mockWriteFileSync.mock.calls[0][1];
124 | 
125 | 		// Verify file extension transformations
126 | 		expect(transformedContent).toContain('file.md');
127 | 		expect(transformedContent).toContain('another.md');
128 | 		expect(transformedContent).toContain('.md extension');
129 | 		expect(transformedContent).not.toContain('.mdc');
130 | 	});
131 | 
132 | 	it('should handle case variations', () => {
133 | 		const testContent = `CURSOR, Cursor, cursor should all be transformed.`;
134 | 
135 | 		mockReadFileSync.mockReturnValue(testContent);
136 | 		mockExistsSync.mockReturnValue(true);
137 | 
138 | 		const result = convertRuleToProfileRule(
139 | 			'test-source.mdc',
140 | 			'test-target.md',
141 | 			kiroProfile
142 | 		);
143 | 
144 | 		expect(result).toBe(true);
145 | 		const transformedContent = mockWriteFileSync.mock.calls[0][1];
146 | 
147 | 		// Verify case transformations
148 | 		// Due to regex order, the case-insensitive rule runs first:
149 | 		// CURSOR -> Kiro (because it starts with 'C'), Cursor -> Kiro, cursor -> kiro
150 | 		expect(transformedContent).toContain('Kiro');
151 | 		expect(transformedContent).toContain('kiro');
152 | 		expect(transformedContent).not.toContain('CURSOR');
153 | 		expect(transformedContent).not.toContain('Cursor');
154 | 		expect(transformedContent).not.toContain('cursor');
155 | 	});
156 | 
157 | 	it('should create target directory if it does not exist', () => {
158 | 		const testContent = 'Test content';
159 | 		mockReadFileSync.mockReturnValue(testContent);
160 | 		mockExistsSync.mockReturnValue(false);
161 | 
162 | 		const result = convertRuleToProfileRule(
163 | 			'test-source.mdc',
164 | 			'nested/path/test-target.md',
165 | 			kiroProfile
166 | 		);
167 | 
168 | 		expect(result).toBe(true);
169 | 		expect(mockMkdirSync).toHaveBeenCalledWith('nested/path', {
170 | 			recursive: true
171 | 		});
172 | 	});
173 | 
174 | 	it('should handle file system errors gracefully', () => {
175 | 		mockReadFileSync.mockImplementation(() => {
176 | 			throw new Error('File not found');
177 | 		});
178 | 
179 | 		const result = convertRuleToProfileRule(
180 | 			'test-source.mdc',
181 | 			'test-target.md',
182 | 			kiroProfile
183 | 		);
184 | 
185 | 		expect(result).toBe(false);
186 | 		expect(mockConsoleError).toHaveBeenCalledWith(
187 | 			'Error converting rule file: File not found'
188 | 		);
189 | 	});
190 | 
191 | 	it('should handle write errors gracefully', () => {
192 | 		mockReadFileSync.mockReturnValue('Test content');
193 | 		mockWriteFileSync.mockImplementation(() => {
194 | 			throw new Error('Write permission denied');
195 | 		});
196 | 
197 | 		const result = convertRuleToProfileRule(
198 | 			'test-source.mdc',
199 | 			'test-target.md',
200 | 			kiroProfile
201 | 		);
202 | 
203 | 		expect(result).toBe(false);
204 | 		expect(mockConsoleError).toHaveBeenCalledWith(
205 | 			'Error converting rule file: Write permission denied'
206 | 		);
207 | 	});
208 | 
209 | 	it('should verify profile configuration', () => {
210 | 		expect(kiroProfile.profileName).toBe('kiro');
211 | 		expect(kiroProfile.displayName).toBe('Kiro');
212 | 		expect(kiroProfile.profileDir).toBe('.kiro');
213 | 		expect(kiroProfile.mcpConfig).toBe(true);
214 | 		expect(kiroProfile.mcpConfigName).toBe('settings/mcp.json');
215 | 		expect(kiroProfile.mcpConfigPath).toBe('.kiro/settings/mcp.json');
216 | 		expect(kiroProfile.includeDefaultRules).toBe(true);
217 | 		expect(kiroProfile.fileMap).toEqual({
218 | 			'rules/cursor_rules.mdc': 'kiro_rules.md',
219 | 			'rules/dev_workflow.mdc': 'dev_workflow.md',
220 | 			'rules/self_improve.mdc': 'self_improve.md',
221 | 			'rules/taskmaster.mdc': 'taskmaster.md',
222 | 			'rules/taskmaster_hooks_workflow.mdc': 'taskmaster_hooks_workflow.md'
223 | 		});
224 | 	});
225 | 
226 | 	describe('onPostConvert lifecycle hook', () => {
227 | 		const mockReaddirSync = jest.spyOn(fs, 'readdirSync');
228 | 		const mockCopyFileSync = jest.spyOn(fs, 'copyFileSync');
229 | 
230 | 		beforeEach(() => {
231 | 			jest.clearAllMocks();
232 | 			// Setup default mock implementation that doesn't throw
233 | 			mockCopyFileSync.mockImplementation(() => {});
234 | 		});
235 | 
236 | 		it('should copy hook files when kiro-hooks directory exists', () => {
237 | 			const projectRoot = '/test/project';
238 | 			const assetsDir = '/test/assets';
239 | 			const hookFiles = [
240 | 				'tm-test-hook1.kiro.hook',
241 | 				'tm-test-hook2.kiro.hook',
242 | 				'not-a-hook.txt'
243 | 			];
244 | 
245 | 			// Mock directory existence
246 | 			mockExistsSync.mockImplementation((path) => {
247 | 				if (path === '/test/assets/kiro-hooks') return true;
248 | 				if (path === '/test/project/.kiro/hooks') return false;
249 | 				return true;
250 | 			});
251 | 
252 | 			// Mock reading hook files
253 | 			mockReaddirSync.mockReturnValue(hookFiles);
254 | 
255 | 			// Call the lifecycle hook
256 | 			kiroProfile.onPostConvertRulesProfile(projectRoot, assetsDir);
257 | 
258 | 			// Verify hooks directory was created
259 | 			expect(mockMkdirSync).toHaveBeenCalledWith('/test/project/.kiro/hooks', {
260 | 				recursive: true
261 | 			});
262 | 
263 | 			// Verify only .kiro.hook files were copied
264 | 			expect(mockCopyFileSync).toHaveBeenCalledTimes(2);
265 | 			expect(mockCopyFileSync).toHaveBeenCalledWith(
266 | 				'/test/assets/kiro-hooks/tm-test-hook1.kiro.hook',
267 | 				'/test/project/.kiro/hooks/tm-test-hook1.kiro.hook'
268 | 			);
269 | 			expect(mockCopyFileSync).toHaveBeenCalledWith(
270 | 				'/test/assets/kiro-hooks/tm-test-hook2.kiro.hook',
271 | 				'/test/project/.kiro/hooks/tm-test-hook2.kiro.hook'
272 | 			);
273 | 		});
274 | 
275 | 		it('should handle case when hooks directory already exists', () => {
276 | 			const projectRoot = '/test/project';
277 | 			const assetsDir = '/test/assets';
278 | 			const hookFiles = ['tm-test-hook.kiro.hook'];
279 | 
280 | 			// Mock all directories exist
281 | 			mockExistsSync.mockReturnValue(true);
282 | 			mockReaddirSync.mockReturnValue(hookFiles);
283 | 
284 | 			// Call the lifecycle hook
285 | 			kiroProfile.onPostConvertRulesProfile(projectRoot, assetsDir);
286 | 
287 | 			// Verify hooks directory was NOT created (already exists)
288 | 			expect(mockMkdirSync).not.toHaveBeenCalled();
289 | 
290 | 			// Verify hook was copied
291 | 			expect(mockCopyFileSync).toHaveBeenCalledWith(
292 | 				'/test/assets/kiro-hooks/tm-test-hook.kiro.hook',
293 | 				'/test/project/.kiro/hooks/tm-test-hook.kiro.hook'
294 | 			);
295 | 		});
296 | 
297 | 		it('should handle case when kiro-hooks source directory does not exist', () => {
298 | 			const projectRoot = '/test/project';
299 | 			const assetsDir = '/test/assets';
300 | 
301 | 			// Mock source directory doesn't exist
302 | 			mockExistsSync.mockImplementation((path) => {
303 | 				if (path === '/test/assets/kiro-hooks') return false;
304 | 				return true;
305 | 			});
306 | 
307 | 			// Call the lifecycle hook
308 | 			kiroProfile.onPostConvertRulesProfile(projectRoot, assetsDir);
309 | 
310 | 			// Verify no files were copied
311 | 			expect(mockReaddirSync).not.toHaveBeenCalled();
312 | 			expect(mockCopyFileSync).not.toHaveBeenCalled();
313 | 		});
314 | 
315 | 		it('should handle case when no hook files exist in source directory', () => {
316 | 			const projectRoot = '/test/project';
317 | 			const assetsDir = '/test/assets';
318 | 
319 | 			// Mock directory exists but has no hook files
320 | 			mockExistsSync.mockReturnValue(true);
321 | 			mockReaddirSync.mockReturnValue(['readme.txt', 'config.json']);
322 | 
323 | 			// Call the lifecycle hook
324 | 			kiroProfile.onPostConvertRulesProfile(projectRoot, assetsDir);
325 | 
326 | 			// Verify no files were copied
327 | 			expect(mockCopyFileSync).not.toHaveBeenCalled();
328 | 		});
329 | 	});
330 | });
331 | 
```

--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import {
  2 | 	isValidProfile,
  3 | 	getRulesProfile
  4 | } from '../../../src/utils/rule-transformer.js';
  5 | import { RULE_PROFILES } from '../../../src/constants/profiles.js';
  6 | import path from 'path';
  7 | 
  8 | describe('Rule Transformer - General', () => {
  9 | 	describe('Profile Configuration Validation', () => {
 10 | 		it('should use RULE_PROFILES as the single source of truth', () => {
 11 | 			// Ensure RULE_PROFILES is properly defined and contains expected profiles
 12 | 			expect(Array.isArray(RULE_PROFILES)).toBe(true);
 13 | 			expect(RULE_PROFILES.length).toBeGreaterThan(0);
 14 | 
 15 | 			// Verify expected profiles are present
 16 | 			const expectedProfiles = [
 17 | 				'claude',
 18 | 				'cline',
 19 | 				'codex',
 20 | 				'cursor',
 21 | 				'gemini',
 22 | 				'kiro',
 23 | 				'opencode',
 24 | 				'roo',
 25 | 				'trae',
 26 | 				'vscode',
 27 | 				'windsurf',
 28 | 				'zed'
 29 | 			];
 30 | 			expectedProfiles.forEach((profile) => {
 31 | 				expect(RULE_PROFILES).toContain(profile);
 32 | 			});
 33 | 		});
 34 | 
 35 | 		it('should validate profiles correctly with isValidProfile', () => {
 36 | 			// Test valid profiles
 37 | 			RULE_PROFILES.forEach((profile) => {
 38 | 				expect(isValidProfile(profile)).toBe(true);
 39 | 			});
 40 | 
 41 | 			// Test invalid profiles
 42 | 			expect(isValidProfile('invalid')).toBe(false);
 43 | 			expect(isValidProfile('')).toBe(false);
 44 | 			expect(isValidProfile(null)).toBe(false);
 45 | 			expect(isValidProfile(undefined)).toBe(false);
 46 | 		});
 47 | 
 48 | 		it('should return correct rule profile with getRulesProfile', () => {
 49 | 			// Test valid profiles
 50 | 			RULE_PROFILES.forEach((profile) => {
 51 | 				const profileConfig = getRulesProfile(profile);
 52 | 				expect(profileConfig).toBeDefined();
 53 | 				expect(profileConfig.profileName.toLowerCase()).toBe(profile);
 54 | 			});
 55 | 
 56 | 			// Test invalid profile - should return null
 57 | 			expect(getRulesProfile('invalid')).toBeNull();
 58 | 		});
 59 | 	});
 60 | 
 61 | 	describe('Profile Structure', () => {
 62 | 		it('should have all required properties for each profile', () => {
 63 | 			RULE_PROFILES.forEach((profile) => {
 64 | 				const profileConfig = getRulesProfile(profile);
 65 | 
 66 | 				// Check required properties
 67 | 				expect(profileConfig).toHaveProperty('profileName');
 68 | 				expect(profileConfig).toHaveProperty('conversionConfig');
 69 | 				expect(profileConfig).toHaveProperty('fileMap');
 70 | 				expect(profileConfig).toHaveProperty('rulesDir');
 71 | 				expect(profileConfig).toHaveProperty('profileDir');
 72 | 
 73 | 				// All profiles should have conversionConfig and fileMap objects
 74 | 				expect(typeof profileConfig.conversionConfig).toBe('object');
 75 | 				expect(typeof profileConfig.fileMap).toBe('object');
 76 | 
 77 | 				// Check that conversionConfig has required structure for profiles with rules
 78 | 				const hasRules = Object.keys(profileConfig.fileMap).length > 0;
 79 | 				if (hasRules) {
 80 | 					expect(profileConfig.conversionConfig).toHaveProperty('profileTerms');
 81 | 					expect(profileConfig.conversionConfig).toHaveProperty('toolNames');
 82 | 					expect(profileConfig.conversionConfig).toHaveProperty('toolContexts');
 83 | 					expect(profileConfig.conversionConfig).toHaveProperty('toolGroups');
 84 | 					expect(profileConfig.conversionConfig).toHaveProperty('docUrls');
 85 | 					expect(profileConfig.conversionConfig).toHaveProperty(
 86 | 						'fileReferences'
 87 | 					);
 88 | 
 89 | 					// Verify arrays are actually arrays
 90 | 					expect(
 91 | 						Array.isArray(profileConfig.conversionConfig.profileTerms)
 92 | 					).toBe(true);
 93 | 					expect(typeof profileConfig.conversionConfig.toolNames).toBe(
 94 | 						'object'
 95 | 					);
 96 | 					expect(
 97 | 						Array.isArray(profileConfig.conversionConfig.toolContexts)
 98 | 					).toBe(true);
 99 | 					expect(Array.isArray(profileConfig.conversionConfig.toolGroups)).toBe(
100 | 						true
101 | 					);
102 | 					expect(Array.isArray(profileConfig.conversionConfig.docUrls)).toBe(
103 | 						true
104 | 					);
105 | 				}
106 | 			});
107 | 		});
108 | 
109 | 		it('should have valid fileMap with required files for each profile', () => {
110 | 			const expectedRuleFiles = [
111 | 				'cursor_rules.mdc',
112 | 				'dev_workflow.mdc',
113 | 				'self_improve.mdc',
114 | 				'taskmaster.mdc'
115 | 			];
116 | 
117 | 			RULE_PROFILES.forEach((profile) => {
118 | 				const profileConfig = getRulesProfile(profile);
119 | 
120 | 				// Check that fileMap exists and is an object
121 | 				expect(profileConfig.fileMap).toBeDefined();
122 | 				expect(typeof profileConfig.fileMap).toBe('object');
123 | 				expect(profileConfig.fileMap).not.toBeNull();
124 | 
125 | 				const fileMapKeys = Object.keys(profileConfig.fileMap);
126 | 
127 | 				// All profiles should have some fileMap entries now
128 | 				expect(fileMapKeys.length).toBeGreaterThan(0);
129 | 
130 | 				// Check if this profile has rule files or asset files
131 | 				const hasRuleFiles = expectedRuleFiles.some((file) =>
132 | 					fileMapKeys.includes(file)
133 | 				);
134 | 				const hasAssetFiles = fileMapKeys.some(
135 | 					(file) => !expectedRuleFiles.includes(file)
136 | 				);
137 | 
138 | 				if (hasRuleFiles) {
139 | 					// Profiles with rule files should have all expected rule files
140 | 					expectedRuleFiles.forEach((expectedFile) => {
141 | 						expect(fileMapKeys).toContain(expectedFile);
142 | 						expect(typeof profileConfig.fileMap[expectedFile]).toBe('string');
143 | 						expect(profileConfig.fileMap[expectedFile].length).toBeGreaterThan(
144 | 							0
145 | 						);
146 | 					});
147 | 				}
148 | 
149 | 				if (hasAssetFiles) {
150 | 					// Profiles with asset files (like Claude/Codex) should have valid asset mappings
151 | 					fileMapKeys.forEach((key) => {
152 | 						expect(typeof profileConfig.fileMap[key]).toBe('string');
153 | 						expect(profileConfig.fileMap[key].length).toBeGreaterThan(0);
154 | 					});
155 | 				}
156 | 			});
157 | 		});
158 | 	});
159 | 
160 | 	describe('MCP Configuration Properties', () => {
161 | 		it('should have all required MCP properties for each profile', () => {
162 | 			RULE_PROFILES.forEach((profile) => {
163 | 				const profileConfig = getRulesProfile(profile);
164 | 
165 | 				// Check MCP-related properties exist
166 | 				expect(profileConfig).toHaveProperty('mcpConfig');
167 | 				expect(profileConfig).toHaveProperty('mcpConfigName');
168 | 				expect(profileConfig).toHaveProperty('mcpConfigPath');
169 | 
170 | 				// Check types based on MCP configuration
171 | 				expect(typeof profileConfig.mcpConfig).toBe('boolean');
172 | 
173 | 				if (profileConfig.mcpConfig !== false) {
174 | 					// Check that mcpConfigPath is properly constructed
175 | 					const expectedPath = path.join(
176 | 						profileConfig.profileDir,
177 | 						profileConfig.mcpConfigName
178 | 					);
179 | 					expect(profileConfig.mcpConfigPath).toBe(expectedPath);
180 | 				}
181 | 			});
182 | 		});
183 | 
184 | 		it('should have correct MCP configuration for each profile', () => {
185 | 			const expectedConfigs = {
186 | 				amp: {
187 | 					mcpConfig: true,
188 | 					mcpConfigName: 'settings.json',
189 | 					expectedPath: '.vscode/settings.json'
190 | 				},
191 | 				claude: {
192 | 					mcpConfig: true,
193 | 					mcpConfigName: '.mcp.json',
194 | 					expectedPath: '.mcp.json'
195 | 				},
196 | 				cline: {
197 | 					mcpConfig: false,
198 | 					mcpConfigName: null,
199 | 					expectedPath: null
200 | 				},
201 | 				codex: {
202 | 					mcpConfig: false,
203 | 					mcpConfigName: null,
204 | 					expectedPath: null
205 | 				},
206 | 				cursor: {
207 | 					mcpConfig: true,
208 | 					mcpConfigName: 'mcp.json',
209 | 					expectedPath: '.cursor/mcp.json'
210 | 				},
211 | 				gemini: {
212 | 					mcpConfig: true,
213 | 					mcpConfigName: 'settings.json',
214 | 					expectedPath: '.gemini/settings.json'
215 | 				},
216 | 				kiro: {
217 | 					mcpConfig: true,
218 | 					mcpConfigName: 'settings/mcp.json',
219 | 					expectedPath: '.kiro/settings/mcp.json'
220 | 				},
221 | 				opencode: {
222 | 					mcpConfig: true,
223 | 					mcpConfigName: 'opencode.json',
224 | 					expectedPath: 'opencode.json'
225 | 				},
226 | 				roo: {
227 | 					mcpConfig: true,
228 | 					mcpConfigName: 'mcp.json',
229 | 					expectedPath: '.roo/mcp.json'
230 | 				},
231 | 				kilo: {
232 | 					mcpConfig: true,
233 | 					mcpConfigName: 'mcp.json',
234 | 					expectedPath: '.kilo/mcp.json'
235 | 				},
236 | 				trae: {
237 | 					mcpConfig: false,
238 | 					mcpConfigName: null,
239 | 					expectedPath: null
240 | 				},
241 | 				vscode: {
242 | 					mcpConfig: true,
243 | 					mcpConfigName: 'mcp.json',
244 | 					expectedPath: '.vscode/mcp.json'
245 | 				},
246 | 				windsurf: {
247 | 					mcpConfig: true,
248 | 					mcpConfigName: 'mcp.json',
249 | 					expectedPath: '.windsurf/mcp.json'
250 | 				},
251 | 				zed: {
252 | 					mcpConfig: true,
253 | 					mcpConfigName: 'settings.json',
254 | 					expectedPath: '.zed/settings.json'
255 | 				}
256 | 			};
257 | 
258 | 			RULE_PROFILES.forEach((profile) => {
259 | 				const profileConfig = getRulesProfile(profile);
260 | 				const expected = expectedConfigs[profile];
261 | 
262 | 				expect(profileConfig.mcpConfig).toBe(expected.mcpConfig);
263 | 				expect(profileConfig.mcpConfigName).toBe(expected.mcpConfigName);
264 | 				expect(profileConfig.mcpConfigPath).toBe(expected.expectedPath);
265 | 			});
266 | 		});
267 | 
268 | 		it('should have consistent profileDir and mcpConfigPath relationship', () => {
269 | 			RULE_PROFILES.forEach((profile) => {
270 | 				const profileConfig = getRulesProfile(profile);
271 | 				if (profileConfig.mcpConfig !== false) {
272 | 					// Profiles with MCP configuration should have valid paths
273 | 					// Handle root directory profiles differently
274 | 					if (profileConfig.profileDir === '.') {
275 | 						if (profile === 'claude') {
276 | 							// Claude explicitly uses '.mcp.json'
277 | 							expect(profileConfig.mcpConfigPath).toBe('.mcp.json');
278 | 						} else {
279 | 							// Other root profiles normalize to just the filename
280 | 							expect(profileConfig.mcpConfigPath).toBe(
281 | 								profileConfig.mcpConfigName
282 | 							);
283 | 						}
284 | 					} else {
285 | 						// Non-root profiles should have profileDir/configName pattern
286 | 						expect(profileConfig.mcpConfigPath).toMatch(
287 | 							new RegExp(
288 | 								`^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`
289 | 							)
290 | 						);
291 | 					}
292 | 				}
293 | 			});
294 | 		});
295 | 
296 | 		it('should have unique profile directories', () => {
297 | 			const profileDirs = RULE_PROFILES.map((profile) => {
298 | 				const profileConfig = getRulesProfile(profile);
299 | 				return profileConfig.profileDir;
300 | 			});
301 | 
302 | 			// Note: Claude and Codex both use "." (root directory) so we expect some duplication
303 | 			const uniqueProfileDirs = [...new Set(profileDirs)];
304 | 			// We should have fewer unique directories than total profiles due to simple profiles using root
305 | 			expect(uniqueProfileDirs.length).toBeLessThanOrEqual(profileDirs.length);
306 | 			expect(uniqueProfileDirs.length).toBeGreaterThan(0);
307 | 		});
308 | 
309 | 		it('should have unique MCP config paths', () => {
310 | 			const mcpConfigPaths = RULE_PROFILES.map((profile) => {
311 | 				const profileConfig = getRulesProfile(profile);
312 | 				return profileConfig.mcpConfigPath;
313 | 			});
314 | 
315 | 			// Note: Claude and Codex both have null mcpConfigPath so we expect some duplication
316 | 			const uniqueMcpConfigPaths = [...new Set(mcpConfigPaths)];
317 | 			// We should have fewer unique paths than total profiles due to simple profiles having null
318 | 			expect(uniqueMcpConfigPaths.length).toBeLessThanOrEqual(
319 | 				mcpConfigPaths.length
320 | 			);
321 | 			expect(uniqueMcpConfigPaths.length).toBeGreaterThan(0);
322 | 		});
323 | 	});
324 | });
325 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/ai/providers/base-provider.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Abstract base provider with Template Method pattern for AI providers
  3 |  * Provides common functionality, error handling, and retry logic
  4 |  */
  5 | 
  6 | import {
  7 | 	ERROR_CODES,
  8 | 	TaskMasterError
  9 | } from '../../../common/errors/task-master-error.js';
 10 | import type {
 11 | 	AIModel,
 12 | 	AIOptions,
 13 | 	AIResponse,
 14 | 	IAIProvider,
 15 | 	ProviderInfo,
 16 | 	ProviderUsageStats
 17 | } from '../interfaces/ai-provider.interface.js';
 18 | 
 19 | // Constants for retry logic
 20 | const DEFAULT_MAX_RETRIES = 3;
 21 | const BASE_RETRY_DELAY_MS = 1000;
 22 | const MAX_RETRY_DELAY_MS = 32000;
 23 | const BACKOFF_MULTIPLIER = 2;
 24 | const JITTER_FACTOR = 0.1;
 25 | 
 26 | // Constants for validation
 27 | const MIN_PROMPT_LENGTH = 1;
 28 | const MAX_PROMPT_LENGTH = 100000;
 29 | const MIN_TEMPERATURE = 0;
 30 | const MAX_TEMPERATURE = 2;
 31 | const MIN_MAX_TOKENS = 1;
 32 | const MAX_MAX_TOKENS = 131072;
 33 | 
 34 | /**
 35 |  * Configuration for BaseProvider
 36 |  */
 37 | export interface BaseProviderConfig {
 38 | 	apiKey: string;
 39 | 	model?: string;
 40 | }
 41 | 
 42 | /**
 43 |  * Internal completion result structure
 44 |  */
 45 | export interface CompletionResult {
 46 | 	content: string;
 47 | 	inputTokens?: number;
 48 | 	outputTokens?: number;
 49 | 	finishReason?: string;
 50 | 	model?: string;
 51 | }
 52 | 
 53 | /**
 54 |  * Validation result for input validation
 55 |  */
 56 | interface ValidationResult {
 57 | 	valid: boolean;
 58 | 	error?: string;
 59 | }
 60 | 
 61 | /**
 62 |  * Prepared request after preprocessing
 63 |  */
 64 | interface PreparedRequest {
 65 | 	prompt: string;
 66 | 	options: AIOptions;
 67 | 	metadata: Record<string, any>;
 68 | }
 69 | 
 70 | /**
 71 |  * Abstract base provider implementing Template Method pattern
 72 |  * Provides common error handling, retry logic, and validation
 73 |  */
 74 | export abstract class BaseProvider implements IAIProvider {
 75 | 	protected readonly apiKey: string;
 76 | 	protected model: string;
 77 | 
 78 | 	constructor(config: BaseProviderConfig) {
 79 | 		if (!config.apiKey) {
 80 | 			throw new TaskMasterError(
 81 | 				'API key is required',
 82 | 				ERROR_CODES.AUTHENTICATION_ERROR
 83 | 			);
 84 | 		}
 85 | 		this.apiKey = config.apiKey;
 86 | 		this.model = config.model || this.getDefaultModel();
 87 | 	}
 88 | 
 89 | 	/**
 90 | 	 * Template method for generating completions
 91 | 	 * Handles validation, retries, and error handling
 92 | 	 */
 93 | 	async generateCompletion(
 94 | 		prompt: string,
 95 | 		options?: AIOptions
 96 | 	): Promise<AIResponse> {
 97 | 		// Validate input
 98 | 		const validation = this.validateInput(prompt, options);
 99 | 		if (!validation.valid) {
100 | 			throw new TaskMasterError(
101 | 				validation.error || 'Invalid input',
102 | 				ERROR_CODES.VALIDATION_ERROR
103 | 			);
104 | 		}
105 | 
106 | 		// Prepare request
107 | 		const prepared = this.prepareRequest(prompt, options);
108 | 
109 | 		// Execute with retry logic
110 | 		let lastError: Error | undefined;
111 | 		const maxRetries = this.getMaxRetries();
112 | 
113 | 		for (let attempt = 1; attempt <= maxRetries; attempt++) {
114 | 			try {
115 | 				const startTime = Date.now();
116 | 				const result = await this.generateCompletionInternal(
117 | 					prepared.prompt,
118 | 					prepared.options
119 | 				);
120 | 
121 | 				const duration = Date.now() - startTime;
122 | 				return this.handleResponse(result, duration, prepared);
123 | 			} catch (error) {
124 | 				lastError = error as Error;
125 | 
126 | 				if (!this.shouldRetry(error, attempt)) {
127 | 					break;
128 | 				}
129 | 
130 | 				const delay = this.calculateBackoffDelay(attempt);
131 | 				await this.sleep(delay);
132 | 			}
133 | 		}
134 | 
135 | 		// All retries failed
136 | 		this.handleError(lastError || new Error('Unknown error'));
137 | 	}
138 | 
139 | 	/**
140 | 	 * Validate input prompt and options
141 | 	 */
142 | 	protected validateInput(
143 | 		prompt: string,
144 | 		options?: AIOptions
145 | 	): ValidationResult {
146 | 		// Validate prompt
147 | 		if (!prompt || typeof prompt !== 'string') {
148 | 			return { valid: false, error: 'Prompt must be a non-empty string' };
149 | 		}
150 | 
151 | 		const trimmedPrompt = prompt.trim();
152 | 		if (trimmedPrompt.length < MIN_PROMPT_LENGTH) {
153 | 			return { valid: false, error: 'Prompt cannot be empty' };
154 | 		}
155 | 
156 | 		if (trimmedPrompt.length > MAX_PROMPT_LENGTH) {
157 | 			return {
158 | 				valid: false,
159 | 				error: `Prompt exceeds maximum length of ${MAX_PROMPT_LENGTH} characters`
160 | 			};
161 | 		}
162 | 
163 | 		// Validate options if provided
164 | 		if (options) {
165 | 			const optionValidation = this.validateOptions(options);
166 | 			if (!optionValidation.valid) {
167 | 				return optionValidation;
168 | 			}
169 | 		}
170 | 
171 | 		return { valid: true };
172 | 	}
173 | 
174 | 	/**
175 | 	 * Validate completion options
176 | 	 */
177 | 	protected validateOptions(options: AIOptions): ValidationResult {
178 | 		if (options.temperature !== undefined) {
179 | 			if (
180 | 				options.temperature < MIN_TEMPERATURE ||
181 | 				options.temperature > MAX_TEMPERATURE
182 | 			) {
183 | 				return {
184 | 					valid: false,
185 | 					error: `Temperature must be between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
186 | 				};
187 | 			}
188 | 		}
189 | 
190 | 		if (options.maxTokens !== undefined) {
191 | 			if (
192 | 				options.maxTokens < MIN_MAX_TOKENS ||
193 | 				options.maxTokens > MAX_MAX_TOKENS
194 | 			) {
195 | 				return {
196 | 					valid: false,
197 | 					error: `Max tokens must be between ${MIN_MAX_TOKENS} and ${MAX_MAX_TOKENS}`
198 | 				};
199 | 			}
200 | 		}
201 | 
202 | 		if (options.topP !== undefined) {
203 | 			if (options.topP < 0 || options.topP > 1) {
204 | 				return { valid: false, error: 'Top-p must be between 0 and 1' };
205 | 			}
206 | 		}
207 | 
208 | 		return { valid: true };
209 | 	}
210 | 
211 | 	/**
212 | 	 * Prepare request for processing
213 | 	 */
214 | 	protected prepareRequest(
215 | 		prompt: string,
216 | 		options?: AIOptions
217 | 	): PreparedRequest {
218 | 		const defaultOptions = this.getDefaultOptions();
219 | 		const mergedOptions = { ...defaultOptions, ...options };
220 | 
221 | 		return {
222 | 			prompt: prompt.trim(),
223 | 			options: mergedOptions,
224 | 			metadata: {
225 | 				provider: this.getName(),
226 | 				model: this.model,
227 | 				timestamp: new Date().toISOString()
228 | 			}
229 | 		};
230 | 	}
231 | 
232 | 	/**
233 | 	 * Process and format the response
234 | 	 */
235 | 	protected handleResponse(
236 | 		result: CompletionResult,
237 | 		duration: number,
238 | 		request: PreparedRequest
239 | 	): AIResponse {
240 | 		const inputTokens =
241 | 			result.inputTokens || this.calculateTokens(request.prompt);
242 | 		const outputTokens =
243 | 			result.outputTokens || this.calculateTokens(result.content);
244 | 
245 | 		return {
246 | 			content: result.content,
247 | 			inputTokens,
248 | 			outputTokens,
249 | 			totalTokens: inputTokens + outputTokens,
250 | 			model: result.model || this.model,
251 | 			provider: this.getName(),
252 | 			timestamp: request.metadata.timestamp,
253 | 			duration,
254 | 			finishReason: result.finishReason
255 | 		};
256 | 	}
257 | 
258 | 	/**
259 | 	 * Handle errors with proper wrapping
260 | 	 */
261 | 	protected handleError(error: unknown): never {
262 | 		if (error instanceof TaskMasterError) {
263 | 			throw error;
264 | 		}
265 | 
266 | 		const errorMessage = error instanceof Error ? error.message : String(error);
267 | 		const errorCode = this.getErrorCode(error);
268 | 
269 | 		throw new TaskMasterError(
270 | 			`${this.getName()} provider error: ${errorMessage}`,
271 | 			errorCode,
272 | 			{
273 | 				operation: 'generateCompletion',
274 | 				resource: this.getName(),
275 | 				details:
276 | 					error instanceof Error
277 | 						? {
278 | 								name: error.name,
279 | 								stack: error.stack,
280 | 								model: this.model
281 | 							}
282 | 						: { error: String(error), model: this.model }
283 | 			},
284 | 			error instanceof Error ? error : undefined
285 | 		);
286 | 	}
287 | 
288 | 	/**
289 | 	 * Determine if request should be retried
290 | 	 */
291 | 	protected shouldRetry(error: unknown, attempt: number): boolean {
292 | 		if (attempt >= this.getMaxRetries()) {
293 | 			return false;
294 | 		}
295 | 
296 | 		return this.isRetryableError(error);
297 | 	}
298 | 
299 | 	/**
300 | 	 * Check if error is retryable
301 | 	 */
302 | 	protected isRetryableError(error: unknown): boolean {
303 | 		if (this.isRateLimitError(error)) return true;
304 | 		if (this.isTimeoutError(error)) return true;
305 | 		if (this.isNetworkError(error)) return true;
306 | 
307 | 		return false;
308 | 	}
309 | 
310 | 	/**
311 | 	 * Check if error is a rate limit error
312 | 	 */
313 | 	protected isRateLimitError(error: unknown): boolean {
314 | 		if (error instanceof Error) {
315 | 			const message = error.message.toLowerCase();
316 | 			return (
317 | 				message.includes('rate limit') ||
318 | 				message.includes('too many requests') ||
319 | 				message.includes('429')
320 | 			);
321 | 		}
322 | 		return false;
323 | 	}
324 | 
325 | 	/**
326 | 	 * Check if error is a timeout error
327 | 	 */
328 | 	protected isTimeoutError(error: unknown): boolean {
329 | 		if (error instanceof Error) {
330 | 			const message = error.message.toLowerCase();
331 | 			return (
332 | 				message.includes('timeout') ||
333 | 				message.includes('timed out') ||
334 | 				message.includes('econnreset')
335 | 			);
336 | 		}
337 | 		return false;
338 | 	}
339 | 
340 | 	/**
341 | 	 * Check if error is a network error
342 | 	 */
343 | 	protected isNetworkError(error: unknown): boolean {
344 | 		if (error instanceof Error) {
345 | 			const message = error.message.toLowerCase();
346 | 			return (
347 | 				message.includes('network') ||
348 | 				message.includes('enotfound') ||
349 | 				message.includes('econnrefused')
350 | 			);
351 | 		}
352 | 		return false;
353 | 	}
354 | 
355 | 	/**
356 | 	 * Calculate exponential backoff delay with jitter
357 | 	 */
358 | 	protected calculateBackoffDelay(attempt: number): number {
359 | 		const exponentialDelay =
360 | 			BASE_RETRY_DELAY_MS * BACKOFF_MULTIPLIER ** (attempt - 1);
361 | 		const clampedDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY_MS);
362 | 
363 | 		// Add jitter to prevent thundering herd
364 | 		const jitter = clampedDelay * JITTER_FACTOR * (Math.random() - 0.5) * 2;
365 | 
366 | 		return Math.round(clampedDelay + jitter);
367 | 	}
368 | 
369 | 	/**
370 | 	 * Get error code from error
371 | 	 */
372 | 	protected getErrorCode(error: unknown): string {
373 | 		if (this.isRateLimitError(error)) return ERROR_CODES.API_ERROR;
374 | 		if (this.isTimeoutError(error)) return ERROR_CODES.NETWORK_ERROR;
375 | 		if (this.isNetworkError(error)) return ERROR_CODES.NETWORK_ERROR;
376 | 
377 | 		if (error instanceof Error && error.message.includes('401')) {
378 | 			return ERROR_CODES.AUTHENTICATION_ERROR;
379 | 		}
380 | 
381 | 		return ERROR_CODES.PROVIDER_ERROR;
382 | 	}
383 | 
384 | 	/**
385 | 	 * Sleep utility for delays
386 | 	 */
387 | 	protected sleep(ms: number): Promise<void> {
388 | 		return new Promise((resolve) => setTimeout(resolve, ms));
389 | 	}
390 | 
391 | 	/**
392 | 	 * Get default options for completions
393 | 	 */
394 | 	protected getDefaultOptions(): AIOptions {
395 | 		return {
396 | 			temperature: 0.7,
397 | 			maxTokens: 2000,
398 | 			topP: 1.0
399 | 		};
400 | 	}
401 | 
402 | 	/**
403 | 	 * Get maximum retry attempts
404 | 	 */
405 | 	protected getMaxRetries(): number {
406 | 		return DEFAULT_MAX_RETRIES;
407 | 	}
408 | 
409 | 	// Public interface methods
410 | 	getModel(): string {
411 | 		return this.model;
412 | 	}
413 | 
414 | 	setModel(model: string): void {
415 | 		this.model = model;
416 | 	}
417 | 
418 | 	// Abstract methods that must be implemented by concrete providers
419 | 	protected abstract generateCompletionInternal(
420 | 		prompt: string,
421 | 		options?: AIOptions
422 | 	): Promise<CompletionResult>;
423 | 
424 | 	abstract calculateTokens(text: string, model?: string): number;
425 | 	abstract getName(): string;
426 | 	abstract getDefaultModel(): string;
427 | 
428 | 	// IAIProvider methods that must be implemented
429 | 	abstract generateStreamingCompletion(
430 | 		prompt: string,
431 | 		options?: AIOptions
432 | 	): AsyncIterator<Partial<AIResponse>>;
433 | 	abstract isAvailable(): Promise<boolean>;
434 | 	abstract getProviderInfo(): ProviderInfo;
435 | 	abstract getAvailableModels(): AIModel[];
436 | 	abstract validateCredentials(): Promise<boolean>;
437 | 	abstract getUsageStats(): Promise<ProviderUsageStats | null>;
438 | 	abstract initialize(): Promise<void>;
439 | 	abstract close(): Promise<void>;
440 | }
441 | 
```

--------------------------------------------------------------------------------
/scripts/modules/task-manager/update-tasks.js:
--------------------------------------------------------------------------------

```javascript
  1 | import path from 'path';
  2 | import chalk from 'chalk';
  3 | import boxen from 'boxen';
  4 | import Table from 'cli-table3';
  5 | 
  6 | import {
  7 | 	log as consoleLog,
  8 | 	readJSON,
  9 | 	writeJSON,
 10 | 	truncate,
 11 | 	isSilentMode
 12 | } from '../utils.js';
 13 | 
 14 | import {
 15 | 	getStatusWithColor,
 16 | 	startLoadingIndicator,
 17 | 	stopLoadingIndicator,
 18 | 	displayAiUsageSummary
 19 | } from '../ui.js';
 20 | 
 21 | import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
 22 | import { getPromptManager } from '../prompt-manager.js';
 23 | import generateTaskFiles from './generate-task-files.js';
 24 | import { generateObjectService } from '../ai-services-unified.js';
 25 | import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
 26 | import { getModelConfiguration } from './models.js';
 27 | import { ContextGatherer } from '../utils/contextGatherer.js';
 28 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
 29 | import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
 30 | 
 31 | /**
 32 |  * Update tasks based on new context using the unified AI service.
 33 |  * @param {string} tasksPath - Path to the tasks.json file
 34 |  * @param {number} fromId - Task ID to start updating from
 35 |  * @param {string} prompt - Prompt with new context
 36 |  * @param {boolean} [useResearch=false] - Whether to use the research AI role.
 37 |  * @param {Object} context - Context object containing session and mcpLog.
 38 |  * @param {Object} [context.session] - Session object from MCP server.
 39 |  * @param {Object} [context.mcpLog] - MCP logger object.
 40 |  * @param {string} [context.tag] - Tag for the task
 41 |  * @param {string} [outputFormat='text'] - Output format ('text' or 'json').
 42 |  */
 43 | async function updateTasks(
 44 | 	tasksPath,
 45 | 	fromId,
 46 | 	prompt,
 47 | 	useResearch = false,
 48 | 	context = {},
 49 | 	outputFormat = 'text' // Default to text for CLI
 50 | ) {
 51 | 	const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
 52 | 	// Use mcpLog if available, otherwise use the imported consoleLog function
 53 | 	const logFn = mcpLog || consoleLog;
 54 | 	// Flag to easily check which logger type we have
 55 | 	const isMCP = !!mcpLog;
 56 | 
 57 | 	if (isMCP)
 58 | 		logFn.info(`updateTasks called with context: session=${!!session}`);
 59 | 	else logFn('info', `updateTasks called`); // CLI log
 60 | 
 61 | 	try {
 62 | 		if (isMCP) logFn.info(`Updating tasks from ID ${fromId}`);
 63 | 		else
 64 | 			logFn(
 65 | 				'info',
 66 | 				`Updating tasks from ID ${fromId} with prompt: "${prompt}"`
 67 | 			);
 68 | 
 69 | 		// Determine project root
 70 | 		const projectRoot = providedProjectRoot || findProjectRoot();
 71 | 		if (!projectRoot) {
 72 | 			throw new Error('Could not determine project root directory');
 73 | 		}
 74 | 
 75 | 		// --- Task Loading/Filtering (Updated to pass projectRoot and tag) ---
 76 | 		const data = readJSON(tasksPath, projectRoot, tag);
 77 | 		if (!data || !data.tasks)
 78 | 			throw new Error(`No valid tasks found in ${tasksPath}`);
 79 | 		const tasksToUpdate = data.tasks.filter(
 80 | 			(task) => task.id >= fromId && task.status !== 'done'
 81 | 		);
 82 | 		if (tasksToUpdate.length === 0) {
 83 | 			if (isMCP)
 84 | 				logFn.info(`No tasks to update (ID >= ${fromId} and not 'done').`);
 85 | 			else
 86 | 				logFn('info', `No tasks to update (ID >= ${fromId} and not 'done').`);
 87 | 			if (outputFormat === 'text') console.log(/* yellow message */);
 88 | 			return; // Nothing to do
 89 | 		}
 90 | 		// --- End Task Loading/Filtering ---
 91 | 
 92 | 		// --- Context Gathering ---
 93 | 		let gatheredContext = '';
 94 | 		try {
 95 | 			const contextGatherer = new ContextGatherer(projectRoot, tag);
 96 | 			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
 97 | 			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update');
 98 | 			const searchResults = fuzzySearch.findRelevantTasks(prompt, {
 99 | 				maxResults: 5,
100 | 				includeSelf: true
101 | 			});
102 | 			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
103 | 
104 | 			const tasksToUpdateIds = tasksToUpdate.map((t) => t.id.toString());
105 | 			const finalTaskIds = [
106 | 				...new Set([...tasksToUpdateIds, ...relevantTaskIds])
107 | 			];
108 | 
109 | 			if (finalTaskIds.length > 0) {
110 | 				const contextResult = await contextGatherer.gather({
111 | 					tasks: finalTaskIds,
112 | 					format: 'research'
113 | 				});
114 | 				gatheredContext = contextResult.context || '';
115 | 			}
116 | 		} catch (contextError) {
117 | 			logFn(
118 | 				'warn',
119 | 				`Could not gather additional context: ${contextError.message}`
120 | 			);
121 | 		}
122 | 		// --- End Context Gathering ---
123 | 
124 | 		// --- Display Tasks to Update (CLI Only - Unchanged) ---
125 | 		if (outputFormat === 'text') {
126 | 			// Show the tasks that will be updated
127 | 			const table = new Table({
128 | 				head: [
129 | 					chalk.cyan.bold('ID'),
130 | 					chalk.cyan.bold('Title'),
131 | 					chalk.cyan.bold('Status')
132 | 				],
133 | 				colWidths: [5, 70, 20]
134 | 			});
135 | 
136 | 			tasksToUpdate.forEach((task) => {
137 | 				table.push([
138 | 					task.id,
139 | 					truncate(task.title, 57),
140 | 					getStatusWithColor(task.status)
141 | 				]);
142 | 			});
143 | 
144 | 			console.log(
145 | 				boxen(chalk.white.bold(`Updating ${tasksToUpdate.length} tasks`), {
146 | 					padding: 1,
147 | 					borderColor: 'blue',
148 | 					borderStyle: 'round',
149 | 					margin: { top: 1, bottom: 0 }
150 | 				})
151 | 			);
152 | 
153 | 			console.log(table.toString());
154 | 
155 | 			// Display a message about how completed subtasks are handled
156 | 			console.log(
157 | 				boxen(
158 | 					chalk.cyan.bold('How Completed Subtasks Are Handled:') +
159 | 						'\n\n' +
160 | 						chalk.white(
161 | 							'• Subtasks marked as "done" or "completed" will be preserved\n'
162 | 						) +
163 | 						chalk.white(
164 | 							'• New subtasks will build upon what has already been completed\n'
165 | 						) +
166 | 						chalk.white(
167 | 							'• If completed work needs revision, a new subtask will be created instead of modifying done items\n'
168 | 						) +
169 | 						chalk.white(
170 | 							'• This approach maintains a clear record of completed work and new requirements'
171 | 						),
172 | 					{
173 | 						padding: 1,
174 | 						borderColor: 'blue',
175 | 						borderStyle: 'round',
176 | 						margin: { top: 1, bottom: 1 }
177 | 					}
178 | 				)
179 | 			);
180 | 		}
181 | 		// --- End Display Tasks ---
182 | 
183 | 		// --- Build Prompts (Using PromptManager) ---
184 | 		// Load prompts using PromptManager
185 | 		const promptManager = getPromptManager();
186 | 		const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
187 | 			'update-tasks',
188 | 			{
189 | 				tasks: tasksToUpdate,
190 | 				updatePrompt: prompt,
191 | 				useResearch,
192 | 				projectContext: gatheredContext,
193 | 				hasCodebaseAnalysis: hasCodebaseAnalysis(
194 | 					useResearch,
195 | 					projectRoot,
196 | 					session
197 | 				),
198 | 				projectRoot: projectRoot
199 | 			}
200 | 		);
201 | 		// --- End Build Prompts ---
202 | 
203 | 		// --- AI Call ---
204 | 		let loadingIndicator = null;
205 | 		let aiServiceResponse = null;
206 | 
207 | 		if (!isMCP && outputFormat === 'text') {
208 | 			loadingIndicator = startLoadingIndicator('Updating tasks with AI...\n');
209 | 		}
210 | 
211 | 		try {
212 | 			// Determine role based on research flag
213 | 			const serviceRole = useResearch ? 'research' : 'main';
214 | 
215 | 			// Call the unified AI service with generateObject
216 | 			aiServiceResponse = await generateObjectService({
217 | 				role: serviceRole,
218 | 				session: session,
219 | 				projectRoot: projectRoot,
220 | 				systemPrompt: systemPrompt,
221 | 				prompt: userPrompt,
222 | 				schema: COMMAND_SCHEMAS['update-tasks'],
223 | 				objectName: 'tasks',
224 | 				commandName: 'update-tasks',
225 | 				outputType: isMCP ? 'mcp' : 'cli'
226 | 			});
227 | 
228 | 			if (loadingIndicator)
229 | 				stopLoadingIndicator(loadingIndicator, 'AI update complete.');
230 | 
231 | 			// With generateObject, we get structured data directly
232 | 			const parsedUpdatedTasks = aiServiceResponse.mainResult.tasks;
233 | 
234 | 			// --- Update Tasks Data (Updated writeJSON call) ---
235 | 			if (!Array.isArray(parsedUpdatedTasks)) {
236 | 				// Should be caught by parser, but extra check
237 | 				throw new Error(
238 | 					'Parsed AI response for updated tasks was not an array.'
239 | 				);
240 | 			}
241 | 			if (isMCP)
242 | 				logFn.info(
243 | 					`Received ${parsedUpdatedTasks.length} updated tasks from AI.`
244 | 				);
245 | 			else
246 | 				logFn(
247 | 					'info',
248 | 					`Received ${parsedUpdatedTasks.length} updated tasks from AI.`
249 | 				);
250 | 			// Create a map for efficient lookup
251 | 			const updatedTasksMap = new Map(
252 | 				parsedUpdatedTasks.map((task) => [task.id, task])
253 | 			);
254 | 
255 | 			let actualUpdateCount = 0;
256 | 			data.tasks.forEach((task, index) => {
257 | 				if (updatedTasksMap.has(task.id)) {
258 | 					// Only update if the task was part of the set sent to AI
259 | 					const updatedTask = updatedTasksMap.get(task.id);
260 | 					// Merge the updated task with the existing one to preserve fields like subtasks
261 | 					data.tasks[index] = {
262 | 						...task, // Keep all existing fields
263 | 						...updatedTask, // Override with updated fields
264 | 						// Ensure subtasks field is preserved if not provided by AI
265 | 						subtasks:
266 | 							updatedTask.subtasks !== undefined
267 | 								? updatedTask.subtasks
268 | 								: task.subtasks
269 | 					};
270 | 					actualUpdateCount++;
271 | 				}
272 | 			});
273 | 			if (isMCP)
274 | 				logFn.info(
275 | 					`Applied updates to ${actualUpdateCount} tasks in the dataset.`
276 | 				);
277 | 			else
278 | 				logFn(
279 | 					'info',
280 | 					`Applied updates to ${actualUpdateCount} tasks in the dataset.`
281 | 				);
282 | 
283 | 			// Fix: Pass projectRoot and currentTag to writeJSON
284 | 			writeJSON(tasksPath, data, projectRoot, tag);
285 | 			if (isMCP)
286 | 				logFn.info(
287 | 					`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
288 | 				);
289 | 			else
290 | 				logFn(
291 | 					'success',
292 | 					`Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
293 | 				);
294 | 			// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
295 | 
296 | 			if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
297 | 				displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
298 | 			}
299 | 
300 | 			return {
301 | 				success: true,
302 | 				updatedTasks: parsedUpdatedTasks,
303 | 				telemetryData: aiServiceResponse.telemetryData,
304 | 				tagInfo: aiServiceResponse.tagInfo
305 | 			};
306 | 		} catch (error) {
307 | 			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
308 | 			if (isMCP) logFn.error(`Error during AI service call: ${error.message}`);
309 | 			else logFn('error', `Error during AI service call: ${error.message}`);
310 | 			if (error.message.includes('API key')) {
311 | 				if (isMCP)
312 | 					logFn.error(
313 | 						'Please ensure API keys are configured correctly in .env or mcp.json.'
314 | 					);
315 | 				else
316 | 					logFn(
317 | 						'error',
318 | 						'Please ensure API keys are configured correctly in .env or mcp.json.'
319 | 					);
320 | 			}
321 | 			throw error;
322 | 		} finally {
323 | 			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
324 | 		}
325 | 	} catch (error) {
326 | 		// --- General Error Handling (Unchanged) ---
327 | 		if (isMCP) logFn.error(`Error updating tasks: ${error.message}`);
328 | 		else logFn('error', `Error updating tasks: ${error.message}`);
329 | 		if (outputFormat === 'text') {
330 | 			console.error(chalk.red(`Error: ${error.message}`));
331 | 			if (getDebugFlag(session)) {
332 | 				console.error(error);
333 | 			}
334 | 			process.exit(1);
335 | 		} else {
336 | 			throw error; // Re-throw for MCP/programmatic callers
337 | 		}
338 | 		// --- End General Error Handling ---
339 | 	}
340 | }
341 | 
342 | export default updateTasks;
343 | 
```
Page 30/69FirstPrevNextLast