This is page 21 of 50. Use http://codebase.md/eyaltoledano/claude-task-master?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/tests/unit/profiles/amp-integration.test.js:
--------------------------------------------------------------------------------
```javascript
import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { getRulesProfile } from '../../../src/utils/rule-transformer.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
describe('Amp Profile Integration', () => {
let tempDir;
let ampProfile;
beforeEach(() => {
// Create temporary directory for testing
tempDir = fs.mkdtempSync(path.join(__dirname, 'temp-amp-unit-'));
// Get the Amp profile
ampProfile = getRulesProfile('amp');
});
afterEach(() => {
// Clean up temporary directory
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('Profile Structure', () => {
test('should have expected profile structure', () => {
expect(ampProfile).toBeDefined();
expect(ampProfile.profileName).toBe('amp');
expect(ampProfile.displayName).toBe('Amp');
expect(ampProfile.profileDir).toBe('.vscode');
expect(ampProfile.rulesDir).toBe('.');
expect(ampProfile.mcpConfig).toBe(true);
expect(ampProfile.mcpConfigName).toBe('settings.json');
expect(ampProfile.mcpConfigPath).toBe('.vscode/settings.json');
expect(ampProfile.includeDefaultRules).toBe(false);
});
test('should have correct file mapping', () => {
expect(ampProfile.fileMap).toEqual({
'AGENTS.md': '.taskmaster/AGENT.md'
});
});
test('should not create unnecessary directories', () => {
// Unlike profiles that copy entire directories, Amp should only create what's needed
const assetsDir = path.join(tempDir, 'assets');
fs.mkdirSync(assetsDir, { recursive: true });
fs.writeFileSync(
path.join(assetsDir, 'AGENTS.md'),
'Task Master instructions'
);
// Call onAddRulesProfile
ampProfile.onAddRulesProfile(tempDir, assetsDir);
// Should only have created .taskmaster directory and AGENT.md
expect(fs.existsSync(path.join(tempDir, '.taskmaster'))).toBe(true);
expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(true);
// Should not have created any other directories (like .claude)
expect(fs.existsSync(path.join(tempDir, '.amp'))).toBe(false);
expect(fs.existsSync(path.join(tempDir, '.claude'))).toBe(false);
});
});
describe('AGENT.md Import Logic', () => {
test('should handle missing source file gracefully', () => {
// Call onAddRulesProfile without creating source file
const assetsDir = path.join(tempDir, 'assets');
fs.mkdirSync(assetsDir, { recursive: true });
// Should not throw error
expect(() => {
ampProfile.onAddRulesProfile(tempDir, assetsDir);
}).not.toThrow();
// Should not create any files
expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(false);
expect(fs.existsSync(path.join(tempDir, '.taskmaster', 'AGENT.md'))).toBe(
false
);
});
test('should preserve existing content when adding import', () => {
// Create existing AGENT.md with specific content
const existingContent =
'# My Custom Amp Setup\n\nThis is my custom configuration.\n\n## Custom Section\n\nSome custom rules here.';
fs.writeFileSync(path.join(tempDir, 'AGENT.md'), existingContent);
// Create mock source
const assetsDir = path.join(tempDir, 'assets');
fs.mkdirSync(assetsDir, { recursive: true });
fs.writeFileSync(
path.join(assetsDir, 'AGENTS.md'),
'Task Master instructions'
);
// Call onAddRulesProfile
ampProfile.onAddRulesProfile(tempDir, assetsDir);
// Check that existing content is preserved
const updatedContent = fs.readFileSync(
path.join(tempDir, 'AGENT.md'),
'utf8'
);
expect(updatedContent).toContain('# My Custom Amp Setup');
expect(updatedContent).toContain('This is my custom configuration.');
expect(updatedContent).toContain('## Custom Section');
expect(updatedContent).toContain('Some custom rules here.');
expect(updatedContent).toContain('@./.taskmaster/AGENT.md');
});
});
describe('MCP Configuration Handling', () => {
test('should handle missing .vscode directory gracefully', () => {
// Call onAddRulesProfile without .vscode directory
const assetsDir = path.join(tempDir, 'assets');
fs.mkdirSync(assetsDir, { recursive: true });
// Should not throw error
expect(() => {
ampProfile.onAddRulesProfile(tempDir, assetsDir);
}).not.toThrow();
});
test('should handle malformed JSON gracefully', () => {
// Create .vscode directory with malformed JSON
const vscodeDirPath = path.join(tempDir, '.vscode');
fs.mkdirSync(vscodeDirPath, { recursive: true });
fs.writeFileSync(
path.join(vscodeDirPath, 'settings.json'),
'{ malformed json'
);
// Should not throw error
expect(() => {
ampProfile.onAddRulesProfile(tempDir, path.join(tempDir, 'assets'));
}).not.toThrow();
});
test('should preserve other VS Code settings when renaming', () => {
// Create .vscode/settings.json with various settings
const vscodeDirPath = path.join(tempDir, '.vscode');
fs.mkdirSync(vscodeDirPath, { recursive: true });
const initialConfig = {
'editor.fontSize': 14,
'editor.tabSize': 2,
mcpServers: {
'task-master-ai': {
command: 'npx',
args: ['-y', 'task-master-ai']
}
},
'workbench.colorTheme': 'Dark+'
};
fs.writeFileSync(
path.join(vscodeDirPath, 'settings.json'),
JSON.stringify(initialConfig, null, '\t')
);
// Call onPostConvertRulesProfile (which handles MCP transformation)
ampProfile.onPostConvertRulesProfile(
tempDir,
path.join(tempDir, 'assets')
);
// Check that other settings are preserved
const settingsFile = path.join(vscodeDirPath, 'settings.json');
const content = fs.readFileSync(settingsFile, 'utf8');
const config = JSON.parse(content);
expect(config['editor.fontSize']).toBe(14);
expect(config['editor.tabSize']).toBe(2);
expect(config['workbench.colorTheme']).toBe('Dark+');
expect(config['amp.mcpServers']).toBeDefined();
expect(config.mcpServers).toBeUndefined();
});
});
describe('Removal Logic', () => {
test('should handle missing files gracefully during removal', () => {
// Should not throw error when removing non-existent files
expect(() => {
ampProfile.onRemoveRulesProfile(tempDir);
}).not.toThrow();
});
test('should handle malformed JSON gracefully during removal', () => {
// Create .vscode directory with malformed JSON
const vscodeDirPath = path.join(tempDir, '.vscode');
fs.mkdirSync(vscodeDirPath, { recursive: true });
fs.writeFileSync(
path.join(vscodeDirPath, 'settings.json'),
'{ malformed json'
);
// Should not throw error
expect(() => {
ampProfile.onRemoveRulesProfile(tempDir);
}).not.toThrow();
});
test('should preserve .vscode directory if it contains other files', () => {
// Create .vscode directory with amp.mcpServers and other files
const vscodeDirPath = path.join(tempDir, '.vscode');
fs.mkdirSync(vscodeDirPath, { recursive: true });
const initialConfig = {
'amp.mcpServers': {
'task-master-ai': {
command: 'npx',
args: ['-y', 'task-master-ai']
}
}
};
fs.writeFileSync(
path.join(vscodeDirPath, 'settings.json'),
JSON.stringify(initialConfig, null, '\t')
);
// Create another file in .vscode
fs.writeFileSync(path.join(vscodeDirPath, 'launch.json'), '{}');
// Call onRemoveRulesProfile
ampProfile.onRemoveRulesProfile(tempDir);
// Check that .vscode directory is preserved
expect(fs.existsSync(vscodeDirPath)).toBe(true);
expect(fs.existsSync(path.join(vscodeDirPath, 'launch.json'))).toBe(true);
});
});
describe('Lifecycle Function Integration', () => {
test('should have all required lifecycle functions', () => {
expect(typeof ampProfile.onAddRulesProfile).toBe('function');
expect(typeof ampProfile.onRemoveRulesProfile).toBe('function');
expect(typeof ampProfile.onPostConvertRulesProfile).toBe('function');
});
test('onPostConvertRulesProfile should behave like onAddRulesProfile', () => {
// Create mock source
const assetsDir = path.join(tempDir, 'assets');
fs.mkdirSync(assetsDir, { recursive: true });
fs.writeFileSync(
path.join(assetsDir, 'AGENTS.md'),
'Task Master instructions'
);
// Call onPostConvertRulesProfile
ampProfile.onPostConvertRulesProfile(tempDir, assetsDir);
// Should have same result as onAddRulesProfile
expect(fs.existsSync(path.join(tempDir, '.taskmaster', 'AGENT.md'))).toBe(
true
);
expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(true);
const agentContent = fs.readFileSync(
path.join(tempDir, 'AGENT.md'),
'utf8'
);
expect(agentContent).toContain('@./.taskmaster/AGENT.md');
});
});
describe('Error Handling', () => {
test('should handle file system errors gracefully', () => {
// Mock fs.writeFileSync to throw an error
const originalWriteFileSync = fs.writeFileSync;
fs.writeFileSync = jest.fn().mockImplementation(() => {
throw new Error('Permission denied');
});
// Create mock source
const assetsDir = path.join(tempDir, 'assets');
fs.mkdirSync(assetsDir, { recursive: true });
originalWriteFileSync.call(
fs,
path.join(assetsDir, 'AGENTS.md'),
'Task Master instructions'
);
// Should not throw error
expect(() => {
ampProfile.onAddRulesProfile(tempDir, assetsDir);
}).not.toThrow();
// Restore original function
fs.writeFileSync = originalWriteFileSync;
});
});
});
```
--------------------------------------------------------------------------------
/docs/mcp-provider.md:
--------------------------------------------------------------------------------
```markdown
# MCP Provider Implementation
## Overview
The MCP Provider creates a modern AI SDK-compliant custom provider that integrates with the existing Task Master MCP server infrastructure. This provider enables AI operations through MCP session sampling while following modern AI SDK patterns and **includes full support for structured object generation (generateObject)** for schema-driven features like PRD parsing and task creation.
## Architecture
### Components
1. **MCPProvider** (`mcp-server/src/providers/mcp-provider.js`)
- Main provider class following Claude Code pattern
- Session-based provider (no API key required)
- Registers with provider registry on MCP server connect
2. **AI SDK Implementation** (`mcp-server/src/custom-sdk/`)
- `index.js` - Provider factory function
- `language-model.js` - LanguageModelV1 implementation with **doGenerateObject support**
- `message-converter.js` - Format conversion utilities
- `json-extractor.js` - **NEW**: Robust JSON extraction from AI responses
- `schema-converter.js` - **NEW**: Schema-to-instructions conversion utility
- `errors.js` - Error handling and mapping
3. **Integration Points**
- MCP Server registration (`mcp-server/src/index.js`)
- AI Services integration (`scripts/modules/ai-services-unified.js`)
- Model configuration (`scripts/modules/supported-models.json`)
### Session Flow
```
MCP Client Connect → MCP Server → registerRemoteProvider()
↓
MCPRemoteProvider (existing)
MCPProvider
↓
Provider Registry
↓
AI Services Layer
↓
Text Generation + Object Generation
```
## Implementation Details
### Provider Registration
The MCP server registers **both** providers when a client connects:
```javascript
// mcp-server/src/index.js
registerRemoteProvider(session) {
if (session?.clientCapabilities?.sampling) {
// Register existing provider
// Register unified MCP provider
const mcpProvider = new MCPProvider();
mcpProvider.setSession(session);
const providerRegistry = ProviderRegistry.getInstance();
providerRegistry.registerProvider('mcp', mcpProvider);
}
}
```
### AI Services Integration
The AI services layer includes the new provider:
```javascript
// scripts/modules/ai-services-unified.js
const PROVIDERS = {
// ... other providers
'mcp': () => {
const providerRegistry = ProviderRegistry.getInstance();
return providerRegistry.getProvider('mcp');
}
};
```
### Message Conversion
The provider converts between AI SDK and MCP formats:
```javascript
// AI SDK prompt → MCP sampling format
const { messages, systemPrompt } = convertToMCPFormat(options.prompt);
// MCP response → AI SDK format
const result = convertFromMCPFormat(response);
```
## Structured Object Generation (generateObject)
### Overview
The MCP Provider includes full support for structured object generation, enabling schema-driven features like PRD parsing, task creation, and any operations requiring validated JSON outputs.
### Architecture
The generateObject implementation includes:
1. **Schema-to-Instructions Conversion** (`schema-converter.js`)
- Converts Zod schemas to natural language instructions
- Generates example outputs to guide AI responses
- Handles complex nested schemas and validation requirements
2. **JSON Extraction Pipeline** (`json-extractor.js`)
- Multiple extraction strategies for robust JSON parsing
- Handles code blocks, malformed JSON, and various response formats
- Fallback mechanisms for maximum reliability
3. **Validation System**
- Complete schema validation using Zod
- Detailed error reporting for failed validations
- Type-safe object generation
### Implementation Details
#### doGenerateObject Method
The `MCPLanguageModel` class implements the AI SDK's `doGenerateObject` method:
```javascript
async doGenerateObject({ schema, objectName, prompt, ...options }) {
// Convert schema to instructions
const instructions = convertSchemaToInstructions(schema, objectName);
// Enhance prompt with structured output requirements
const enhancedPrompt = enhancePromptForObjectGeneration(prompt, instructions);
// Generate response via MCP sampling
const response = await this.doGenerate({ prompt: enhancedPrompt, ...options });
// Extract and validate JSON
const extractedJson = extractJsonFromResponse(response.text);
const validatedObject = schema.parse(extractedJson);
return {
object: validatedObject,
usage: response.usage,
finishReason: response.finishReason
};
}
```
#### AI SDK Compatibility
The provider includes required properties for AI SDK object generation:
```javascript
class MCPLanguageModel {
get defaultObjectGenerationMode() {
return 'tool';
}
get supportsStructuredOutputs() {
return true;
}
// ... doGenerateObject implementation
}
```
### Usage Examples
#### PRD Parsing
```javascript
import { z } from 'zod';
const taskSchema = z.object({
title: z.string(),
description: z.string(),
priority: z.enum(['high', 'medium', 'low']),
dependencies: z.array(z.number()).optional()
});
const result = await generateObject({
model: mcpModel,
schema: taskSchema,
prompt: 'Parse this PRD section into a task: [PRD content]'
});
console.log(result.object); // Validated task object
```
#### Task Creation
```javascript
const taskCreationSchema = z.object({
task: z.object({
title: z.string(),
description: z.string(),
details: z.string(),
testStrategy: z.string(),
priority: z.enum(['high', 'medium', 'low']),
dependencies: z.array(z.number()).optional()
})
});
const result = await generateObject({
model: mcpModel,
schema: taskCreationSchema,
prompt: 'Create a comprehensive task for implementing user authentication'
});
```
### Error Handling
The implementation provides comprehensive error handling:
- **Schema Validation Errors**: Detailed Zod validation messages
- **JSON Extraction Failures**: Fallback strategies and clear error reporting
- **MCP Communication Errors**: Proper error mapping and recovery
- **Timeout Handling**: Configurable timeouts for long-running operations
### Testing
The generateObject functionality is fully tested:
```bash
# Test object generation
npm test -- --grep "generateObject"
# Test with actual MCP session
node test-object-generation.js
```
### Supported Features
✅ **Schema Conversion**: Zod schemas → Natural language instructions
✅ **JSON Extraction**: Multiple strategies for robust parsing
✅ **Validation**: Complete schema validation with error reporting
✅ **Error Recovery**: Fallback mechanisms for failed extractions
✅ **Type Safety**: Full TypeScript support with inferred types
✅ **AI SDK Compliance**: Complete LanguageModelV1 interface implementation
## Usage
### Configuration
Add to supported models configuration:
```json
{
"mcp": [
{
"id": "claude-3-5-sonnet-20241022",
"swe_score": 0.623,
"cost_per_1m_tokens": { "input": 0, "output": 0 },
"allowed_roles": ["main", "fallback", "research"],
"max_tokens": 200000
}
]
}
```
### CLI Usage
```bash
# Set provider for main role
tm models set-main --provider mcp --model claude-3-5-sonnet-20241022
# Use in task operations
tm add-task "Create user authentication system"
```
### Programmatic Usage
```javascript
const provider = registry.getProvider('mcp');
if (provider && provider.hasValidSession()) {
const client = provider.getClient({ temperature: 0.7 });
const model = client({ modelId: 'claude-3-5-sonnet-20241022' });
const result = await model.doGenerate({
prompt: [
{ role: 'user', content: 'Hello!' }
]
});
}
```
## Testing
### Component Tests
```bash
# Test individual components
node test-mcp-components.js
```
### Integration Testing
1. Start MCP server
2. Connect Claude client
3. Verify both providers are registered
4. Test AI operations through mcp provider
### Validation Checklist
- ✅ Provider creation and initialization
- ✅ Registry integration
- ✅ Session management
- ✅ Message conversion
- ✅ Error handling
- ✅ AI Services integration
- ✅ Model configuration
## Key Benefits
1. **AI SDK Compliance** - Full LanguageModelV1 implementation
2. **Session Integration** - Leverages existing MCP session infrastructure
3. **Registry Pattern** - Uses provider registry for discovery
4. **Backward Compatibility** - Coexists with existing MCPRemoteProvider
5. **Future Ready** - Supports AI SDK features and patterns
## Troubleshooting
### Provider Not Found
```
Error: Provider "mcp" not found in registry
```
**Solution**: Ensure MCP server is running and client is connected
### Session Errors
```
Error: MCP Provider requires active MCP session
```
**Solution**: Check MCP client connection and session capabilities
### Sampling Errors
```
Error: MCP session must have client sampling capabilities
```
**Solution**: Verify MCP client supports sampling operations
## Next Steps
1. **Performance Optimization** - Add caching and connection pooling
2. **Enhanced Streaming** - Implement native streaming if MCP supports it
3. **Tool Integration** - Add support for function calling through MCP tools
4. **Monitoring** - Add metrics and logging for provider usage
5. **Documentation** - Update user guides and API documentation
```
--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer-vscode.test.js:
--------------------------------------------------------------------------------
```javascript
import { jest } from '@jest/globals';
// Mock fs module before importing anything that uses it
jest.mock('fs', () => ({
readFileSync: jest.fn(),
writeFileSync: jest.fn(),
existsSync: jest.fn(),
mkdirSync: jest.fn()
}));
// Import modules after mocking
import fs from 'fs';
import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
import { vscodeProfile } from '../../../src/profiles/vscode.js';
describe('VS Code Rule Transformer', () => {
// Set up spies on the mocked modules
const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
const mockExistsSync = jest.spyOn(fs, 'existsSync');
const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
const mockConsoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
beforeEach(() => {
jest.clearAllMocks();
// Setup default mocks
mockReadFileSync.mockReturnValue('');
mockWriteFileSync.mockImplementation(() => {});
mockExistsSync.mockReturnValue(true);
mockMkdirSync.mockImplementation(() => {});
});
afterAll(() => {
jest.restoreAllMocks();
});
it('should correctly convert basic terms', () => {
const testContent = `---
description: Test Cursor rule for basic terms
globs: **/*
alwaysApply: true
---
This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
Also has references to .mdc files and cursor rules.`;
// Mock file read to return our test content
mockReadFileSync.mockReturnValue(testContent);
// Call the actual function
const result = convertRuleToProfileRule(
'source.mdc',
'target.md',
vscodeProfile
);
// Verify the function succeeded
expect(result).toBe(true);
// Verify file operations were called correctly
expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8');
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
// Get the transformed content that was written
const writeCall = mockWriteFileSync.mock.calls[0];
const transformedContent = writeCall[1];
// Verify transformations
expect(transformedContent).toContain('VS Code');
expect(transformedContent).toContain('code.visualstudio.com');
expect(transformedContent).toContain('.md');
expect(transformedContent).toContain('vscode rules'); // "cursor rules" -> "vscode rules"
expect(transformedContent).toContain('applyTo: "**/*"'); // globs -> applyTo transformation
expect(transformedContent).not.toContain('cursor.so');
expect(transformedContent).not.toContain('Cursor rule');
expect(transformedContent).not.toContain('globs:');
expect(transformedContent).not.toContain('alwaysApply:');
});
it('should correctly convert tool references', () => {
const testContent = `---
description: Test Cursor rule for tool references
globs: **/*
alwaysApply: true
---
- Use the search tool to find code
- The edit_file tool lets you modify files
- run_command executes terminal commands
- use_mcp connects to external services`;
// Mock file read to return our test content
mockReadFileSync.mockReturnValue(testContent);
// Call the actual function
const result = convertRuleToProfileRule(
'source.mdc',
'target.md',
vscodeProfile
);
// Verify the function succeeded
expect(result).toBe(true);
// Get the transformed content that was written
const writeCall = mockWriteFileSync.mock.calls[0];
const transformedContent = writeCall[1];
// Verify transformations (VS Code uses standard tool names, so no transformation)
expect(transformedContent).toContain('search tool');
expect(transformedContent).toContain('edit_file tool');
expect(transformedContent).toContain('run_command');
expect(transformedContent).toContain('use_mcp');
expect(transformedContent).toContain('applyTo: "**/*"'); // globs -> applyTo transformation
});
it('should correctly update file references and directory paths', () => {
const testContent = `---
description: Test Cursor rule for file references
globs: .cursor/rules/*.md
alwaysApply: true
---
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).
Files are in the .cursor/rules directory and we should reference the rules directory.`;
// Mock file read to return our test content
mockReadFileSync.mockReturnValue(testContent);
// Call the actual function
const result = convertRuleToProfileRule(
'source.mdc',
'target.instructions.md',
vscodeProfile
);
// Verify the function succeeded
expect(result).toBe(true);
// Get the transformed content that was written
const writeCall = mockWriteFileSync.mock.calls[0];
const transformedContent = writeCall[1];
// Verify transformations specific to VS Code
expect(transformedContent).toContain(
'applyTo: ".github/instructions/*.md"'
); // globs -> applyTo with path transformation
expect(transformedContent).toContain(
'(.github/instructions/dev_workflow.instructions.md)'
); // File path transformation - no taskmaster subdirectory for VS Code
expect(transformedContent).toContain(
'(.github/instructions/taskmaster.instructions.md)'
); // File path transformation - no taskmaster subdirectory for VS Code
expect(transformedContent).toContain('instructions directory'); // "rules directory" -> "instructions directory"
expect(transformedContent).not.toContain('(mdc:.cursor/rules/');
expect(transformedContent).not.toContain('.cursor/rules');
expect(transformedContent).not.toContain('globs:');
expect(transformedContent).not.toContain('rules directory');
});
it('should transform globs to applyTo with various patterns', () => {
const testContent = `---
description: Test VS Code applyTo transformation
globs: .cursor/rules/*.md
alwaysApply: true
---
Another section:
globs: **/*.ts
final: true
Last one:
globs: src/**/*
---`;
// Mock file read to return our test content
mockReadFileSync.mockReturnValue(testContent);
// Call the actual function
const result = convertRuleToProfileRule(
'source.mdc',
'target.md',
vscodeProfile
);
// Verify the function succeeded
expect(result).toBe(true);
// Get the transformed content that was written
const writeCall = mockWriteFileSync.mock.calls[0];
const transformedContent = writeCall[1];
// Verify all globs transformations
expect(transformedContent).toContain(
'applyTo: ".github/instructions/*.md"'
); // Path transformation applied
expect(transformedContent).toContain('applyTo: "**/*.ts"'); // Pattern with quotes
expect(transformedContent).toContain('applyTo: "src/**/*"'); // Complex pattern with quotes
expect(transformedContent).not.toContain('globs:'); // No globs should remain
});
it('should handle VS Code MCP configuration paths correctly', () => {
const testContent = `---
description: Test MCP configuration paths
globs: **/*
alwaysApply: true
---
MCP configuration is at .cursor/mcp.json for Cursor.
The .cursor/rules directory contains rules.
Update your .cursor/mcp.json file accordingly.`;
// Mock file read to return our test content
mockReadFileSync.mockReturnValue(testContent);
// Call the actual function
const result = convertRuleToProfileRule(
'source.mdc',
'target.md',
vscodeProfile
);
// Verify the function succeeded
expect(result).toBe(true);
// Get the transformed content that was written
const writeCall = mockWriteFileSync.mock.calls[0];
const transformedContent = writeCall[1];
// Verify MCP paths are correctly transformed
expect(transformedContent).toContain('.vscode/mcp.json'); // MCP config in .vscode
expect(transformedContent).toContain('.github/instructions'); // Rules/instructions in .github/instructions
expect(transformedContent).not.toContain('.cursor/mcp.json');
expect(transformedContent).not.toContain('.cursor/rules');
});
it('should handle file read errors', () => {
// Mock file read to throw an error
mockReadFileSync.mockImplementation(() => {
throw new Error('File not found');
});
// Call the actual function
const result = convertRuleToProfileRule(
'nonexistent.mdc',
'target.md',
vscodeProfile
);
// Verify the function failed gracefully
expect(result).toBe(false);
// Verify writeFileSync was not called
expect(mockWriteFileSync).not.toHaveBeenCalled();
// Verify error was logged
expect(mockConsoleError).toHaveBeenCalledWith(
'Error converting rule file: File not found'
);
});
it('should handle file write errors', () => {
const testContent = 'test content';
mockReadFileSync.mockReturnValue(testContent);
// Mock file write to throw an error
mockWriteFileSync.mockImplementation(() => {
throw new Error('Permission denied');
});
// Call the actual function
const result = convertRuleToProfileRule(
'source.mdc',
'target.md',
vscodeProfile
);
// Verify the function failed gracefully
expect(result).toBe(false);
// Verify error was logged
expect(mockConsoleError).toHaveBeenCalledWith(
'Error converting rule file: Permission denied'
);
});
it('should create target directory if it does not exist', () => {
const testContent = 'test content';
mockReadFileSync.mockReturnValue(testContent);
// Mock directory doesn't exist initially
mockExistsSync.mockReturnValue(false);
// Call the actual function
convertRuleToProfileRule(
'source.mdc',
'.github/instructions/deep/path/target.md',
vscodeProfile
);
// Verify directory creation was called
expect(mockMkdirSync).toHaveBeenCalledWith(
'.github/instructions/deep/path',
{
recursive: true
}
);
});
});
```
--------------------------------------------------------------------------------
/apps/cli/src/commands/briefs.command.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Briefs Command - Friendly alias for tag management in API storage
* Provides brief-specific commands that only work with API storage
*/
import {
type LogLevel,
type TagInfo,
tryAddTagViaRemote,
tryListTagsViaRemote
} from '@tm/bridge';
import type { TmCore } from '@tm/core';
import { AuthManager, createTmCore } from '@tm/core';
import { Command } from 'commander';
import { checkAuthentication } from '../utils/auth-helpers.js';
import {
selectBriefFromInput,
selectBriefInteractive
} from '../utils/brief-selection.js';
import * as ui from '../utils/ui.js';
/**
* Result type from briefs command
*/
export interface BriefsResult {
success: boolean;
action: 'list' | 'select' | 'create';
briefs?: TagInfo[];
currentBrief?: string | null;
message?: string;
}
/**
* BriefsCommand - Manage briefs for API storage (friendly alias)
* Only works when using API storage (tryhamster.com)
*/
export class BriefsCommand extends Command {
private tmCore?: TmCore;
private authManager: AuthManager;
private lastResult?: BriefsResult;
constructor(name?: string) {
super(name || 'briefs');
// Initialize auth manager
this.authManager = AuthManager.getInstance();
// Configure the command
this.description('Manage briefs (API storage only)');
this.alias('brief');
// Add subcommands
this.addListCommand();
this.addSelectCommand();
this.addCreateCommand();
// Accept optional positional argument for brief URL/ID
this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL');
// Default action: if argument provided, select brief; else list briefs
this.action(async (briefOrUrl?: string) => {
if (briefOrUrl && briefOrUrl.trim().length > 0) {
await this.executeSelectFromUrl(briefOrUrl.trim());
return;
}
await this.executeList();
});
}
/**
* Check if user is authenticated (required for briefs)
*/
private async checkAuth(): Promise<boolean> {
return checkAuthentication(this.authManager, {
message:
'The "briefs" command requires you to be logged in to your Hamster account.',
footer:
'Working locally instead?\n' +
' → Use "task-master tags" for local tag management.',
authCommand: 'task-master auth login'
});
}
/**
* Add list subcommand
*/
private addListCommand(): void {
this.command('list')
.description('List all briefs (default action)')
.option('--show-metadata', 'Show additional brief metadata')
.addHelpText(
'after',
`
Examples:
$ tm briefs # List all briefs (default)
$ tm briefs list # List all briefs (explicit)
$ tm briefs list --show-metadata # List with metadata
Note: This command only works with API storage (tryhamster.com).
`
)
.action(async (options) => {
await this.executeList(options);
});
}
/**
* Add select subcommand
*/
private addSelectCommand(): void {
this.command('select')
.description('Select a brief to work with')
.argument(
'[briefOrUrl]',
'Brief ID or Hamster URL (optional, interactive if omitted)'
)
.addHelpText(
'after',
`
Examples:
$ tm brief select # Interactive selection
$ tm brief select abc12345 # Select by ID
$ tm brief select https://app.tryhamster.com/... # Select by URL
Shortcuts:
$ tm brief <brief-url> # Same as "select"
$ tm brief # List all briefs
Note: Works exactly like "tm context brief" - reuses the same interactive interface.
`
)
.action(async (briefOrUrl) => {
await this.executeSelect(briefOrUrl);
});
}
/**
* Add create subcommand
*/
private addCreateCommand(): void {
this.command('create')
.description('Create a new brief (redirects to web UI)')
.argument('[name]', 'Brief name (optional)')
.addHelpText(
'after',
`
Examples:
$ tm briefs create # Redirect to web UI to create brief
$ tm briefs create my-new-brief # Redirect with suggested name
Note: Briefs must be created through the Hamster Studio web interface.
`
)
.action(async (name) => {
await this.executeCreate(name);
});
}
/**
* Initialize TmCore if not already initialized
*/
private async initTmCore(): Promise<void> {
if (!this.tmCore) {
this.tmCore = await createTmCore({
projectPath: process.cwd()
});
}
}
/**
* Execute list briefs
*/
private async executeList(options?: {
showMetadata?: boolean;
}): Promise<void> {
try {
// Check authentication
if (!(await this.checkAuth())) {
process.exit(1);
}
// Use the bridge to list briefs
const remoteResult = await tryListTagsViaRemote({
projectRoot: process.cwd(),
showMetadata: options?.showMetadata || false,
report: (level: LogLevel, ...args: unknown[]) => {
const message = args[0] as string;
if (level === 'error') ui.displayError(message);
else if (level === 'warn') ui.displayWarning(message);
else if (level === 'info') ui.displayInfo(message);
}
});
if (!remoteResult) {
throw new Error('Failed to fetch briefs from API');
}
this.setLastResult({
success: remoteResult.success,
action: 'list',
briefs: remoteResult.tags,
currentBrief: remoteResult.currentTag,
message: remoteResult.message
});
} catch (error) {
ui.displayError(`Failed to list briefs: ${(error as Error).message}`);
this.setLastResult({
success: false,
action: 'list',
message: (error as Error).message
});
process.exit(1);
}
}
/**
* Execute select brief interactively or by name/ID
*/
private async executeSelect(nameOrId?: string): Promise<void> {
try {
// Check authentication
const hasSession = await this.authManager.hasValidSession();
if (!hasSession) {
ui.displayError('Not authenticated. Run "tm auth login" first.');
process.exit(1);
}
// If name/ID provided, treat it as URL/ID selection
if (nameOrId && nameOrId.trim().length > 0) {
await this.executeSelectFromUrl(nameOrId.trim());
return;
}
// Check if org is selected for interactive selection
const context = this.authManager.getContext();
if (!context?.orgId) {
ui.displayErrorBox(
'No organization selected. Run "tm context org" first.'
);
process.exit(1);
}
// Use shared utility for interactive selection
const result = await selectBriefInteractive(
this.authManager,
context.orgId
);
this.setLastResult({
success: result.success,
action: 'select',
currentBrief: result.briefId,
message: result.message
});
if (!result.success) {
process.exit(1);
}
} catch (error) {
ui.displayErrorBox(`Failed to select brief: ${(error as Error).message}`);
this.setLastResult({
success: false,
action: 'select',
message: (error as Error).message
});
process.exit(1);
}
}
/**
* Execute select brief from any input (URL, ID, or name)
* All parsing logic is in tm-core
*/
private async executeSelectFromUrl(input: string): Promise<void> {
try {
// Check authentication
const hasSession = await this.authManager.hasValidSession();
if (!hasSession) {
ui.displayError('Not authenticated. Run "tm auth login" first.');
process.exit(1);
}
// Initialize tmCore to access business logic
await this.initTmCore();
// Use shared utility - tm-core handles ALL parsing
const result = await selectBriefFromInput(
this.authManager,
input,
this.tmCore
);
this.setLastResult({
success: result.success,
action: 'select',
currentBrief: result.briefId,
message: result.message
});
if (!result.success) {
process.exit(1);
}
} catch (error) {
ui.displayErrorBox(`Failed to select brief: ${(error as Error).message}`);
this.setLastResult({
success: false,
action: 'select',
message: (error as Error).message
});
process.exit(1);
}
}
/**
* Execute create brief (redirect to web UI)
*/
private async executeCreate(name?: string): Promise<void> {
try {
// Check authentication
if (!(await this.checkAuth())) {
process.exit(1);
}
// Use the bridge to redirect to web UI
const remoteResult = await tryAddTagViaRemote({
tagName: name || 'new-brief',
projectRoot: process.cwd(),
report: (level: LogLevel, ...args: unknown[]) => {
const message = args[0] as string;
if (level === 'error') ui.displayError(message);
else if (level === 'warn') ui.displayWarning(message);
else if (level === 'info') ui.displayInfo(message);
}
});
if (!remoteResult) {
throw new Error('Failed to get brief creation URL');
}
this.setLastResult({
success: remoteResult.success,
action: 'create',
message: remoteResult.message
});
if (!remoteResult.success) {
process.exit(1);
}
} catch (error) {
ui.displayErrorBox(`Failed to create brief: ${(error as Error).message}`);
this.setLastResult({
success: false,
action: 'create',
message: (error as Error).message
});
process.exit(1);
}
}
/**
* Set the last result for programmatic access
*/
private setLastResult(result: BriefsResult): void {
this.lastResult = result;
}
/**
* Get the last result (for programmatic usage)
*/
getLastResult(): BriefsResult | undefined {
return this.lastResult;
}
/**
* Register this command on an existing program
*/
static register(program: Command, name?: string): BriefsCommand {
const briefsCommand = new BriefsCommand(name);
program.addCommand(briefsCommand);
return briefsCommand;
}
}
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/tasks/services/preflight-checker.service.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Preflight Checker Service
* Validates environment and prerequisites for autopilot execution
*/
import { readFileSync, existsSync, readdirSync } from 'node:fs';
import { join } from 'path';
import { execSync } from 'child_process';
import { getLogger } from '../../../common/logger/factory.js';
import {
isGitRepository,
isGhCliAvailable,
getDefaultBranch
} from '../../../common/utils/git-utils.js';
const logger = getLogger('PreflightChecker');
/**
* Result of a single preflight check
*/
export interface CheckResult {
/** Whether the check passed */
success: boolean;
/** The value detected/validated */
value?: any;
/** Error or warning message */
message?: string;
}
/**
* Complete preflight validation results
*/
export interface PreflightResult {
/** Overall success - all checks passed */
success: boolean;
/** Test command detection result */
testCommand: CheckResult;
/** Git working tree status */
gitWorkingTree: CheckResult;
/** Required tools availability */
requiredTools: CheckResult;
/** Default branch detection */
defaultBranch: CheckResult;
/** Summary message */
summary: string;
}
/**
* Tool validation result
*/
interface ToolCheck {
name: string;
available: boolean;
version?: string;
message?: string;
}
/**
* PreflightChecker validates environment for autopilot execution
*/
export class PreflightChecker {
private projectRoot: string;
constructor(projectRoot: string) {
if (!projectRoot) {
throw new Error('projectRoot is required for PreflightChecker');
}
this.projectRoot = projectRoot;
}
/**
* Detect test command from package.json
*/
async detectTestCommand(): Promise<CheckResult> {
try {
const packageJsonPath = join(this.projectRoot, 'package.json');
const packageJsonContent = readFileSync(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(packageJsonContent);
if (!packageJson.scripts || !packageJson.scripts.test) {
return {
success: false,
message:
'No test script found in package.json. Please add a "test" script.'
};
}
const testCommand = packageJson.scripts.test;
return {
success: true,
value: testCommand,
message: `Test command: ${testCommand}`
};
} catch (error: any) {
if (error.code === 'ENOENT') {
return {
success: false,
message: 'package.json not found in project root'
};
}
return {
success: false,
message: `Failed to read package.json: ${error.message}`
};
}
}
/**
* Check git working tree status
*/
async checkGitWorkingTree(): Promise<CheckResult> {
try {
// Check if it's a git repository
const isRepo = await isGitRepository(this.projectRoot);
if (!isRepo) {
return {
success: false,
message: 'Not a git repository. Initialize git first.'
};
}
// Check for changes (staged/unstaged/untracked) without requiring HEAD
const status = execSync('git status --porcelain', {
cwd: this.projectRoot,
encoding: 'utf-8',
timeout: 5000
});
if (status.trim().length > 0) {
return {
success: false,
value: 'dirty',
message:
'Working tree has uncommitted or untracked changes. Please commit or stash them.'
};
}
return {
success: true,
value: 'clean',
message: 'Working tree is clean'
};
} catch (error: any) {
return {
success: false,
message: `Git check failed: ${error.message}`
};
}
}
/**
* Detect project types based on common configuration files
*/
private detectProjectTypes(): string[] {
const types: string[] = [];
if (existsSync(join(this.projectRoot, 'package.json'))) types.push('node');
if (
existsSync(join(this.projectRoot, 'requirements.txt')) ||
existsSync(join(this.projectRoot, 'setup.py')) ||
existsSync(join(this.projectRoot, 'pyproject.toml'))
)
types.push('python');
if (
existsSync(join(this.projectRoot, 'pom.xml')) ||
existsSync(join(this.projectRoot, 'build.gradle'))
)
types.push('java');
if (existsSync(join(this.projectRoot, 'go.mod'))) types.push('go');
if (existsSync(join(this.projectRoot, 'Cargo.toml'))) types.push('rust');
if (existsSync(join(this.projectRoot, 'composer.json'))) types.push('php');
if (existsSync(join(this.projectRoot, 'Gemfile'))) types.push('ruby');
const files = readdirSync(this.projectRoot);
if (files.some((f) => f.endsWith('.csproj') || f.endsWith('.sln')))
types.push('dotnet');
return types;
}
/**
* Get required tools for a project type
*/
private getToolsForProjectType(
type: string
): Array<{ command: string; args: string[] }> {
const toolMap: Record<
string,
Array<{ command: string; args: string[] }>
> = {
node: [
{ command: 'node', args: ['--version'] },
{ command: 'npm', args: ['--version'] }
],
python: [
{ command: 'python3', args: ['--version'] },
{ command: 'pip3', args: ['--version'] }
],
java: [{ command: 'java', args: ['--version'] }],
go: [{ command: 'go', args: ['version'] }],
rust: [{ command: 'cargo', args: ['--version'] }],
php: [
{ command: 'php', args: ['--version'] },
{ command: 'composer', args: ['--version'] }
],
ruby: [
{ command: 'ruby', args: ['--version'] },
{ command: 'bundle', args: ['--version'] }
],
dotnet: [{ command: 'dotnet', args: ['--version'] }]
};
return toolMap[type] || [];
}
/**
* Validate required tools availability
*/
async validateRequiredTools(): Promise<CheckResult> {
const tools: ToolCheck[] = [];
// Always check git and gh CLI
tools.push(this.checkTool('git', ['--version']));
tools.push(await this.checkGhCli());
// Detect project types and check their tools
const projectTypes = this.detectProjectTypes();
if (projectTypes.length === 0) {
logger.warn('No recognized project type detected');
} else {
logger.info(`Detected project types: ${projectTypes.join(', ')}`);
}
for (const type of projectTypes) {
const typeTools = this.getToolsForProjectType(type);
for (const tool of typeTools) {
tools.push(this.checkTool(tool.command, tool.args));
}
}
// Determine overall success
const allAvailable = tools.every((tool) => tool.available);
const missingTools = tools
.filter((tool) => !tool.available)
.map((tool) => tool.name);
if (!allAvailable) {
return {
success: false,
value: tools,
message: `Missing required tools: ${missingTools.join(', ')}`
};
}
return {
success: true,
value: tools,
message: 'All required tools are available'
};
}
/**
* Check if a command-line tool is available
*/
private checkTool(command: string, versionArgs: string[]): ToolCheck {
try {
const version = execSync(`${command} ${versionArgs.join(' ')}`, {
cwd: this.projectRoot,
encoding: 'utf-8',
stdio: 'pipe',
timeout: 5000
})
.trim()
.split('\n')[0];
return {
name: command,
available: true,
version,
message: `${command} ${version}`
};
} catch (error) {
return {
name: command,
available: false,
message: `${command} not found`
};
}
}
/**
* Check GitHub CLI installation and authentication status
*/
private async checkGhCli(): Promise<ToolCheck> {
try {
const version = execSync('gh --version', {
cwd: this.projectRoot,
encoding: 'utf-8',
stdio: 'pipe',
timeout: 5000
})
.trim()
.split('\n')[0];
const authed = await isGhCliAvailable(this.projectRoot);
return {
name: 'gh',
available: true,
version,
message: authed
? 'GitHub CLI installed (authenticated)'
: 'GitHub CLI installed (not authenticated)'
};
} catch {
return { name: 'gh', available: false, message: 'GitHub CLI not found' };
}
}
/**
* Detect default branch
*/
async detectDefaultBranch(): Promise<CheckResult> {
try {
const defaultBranch = await getDefaultBranch(this.projectRoot);
if (!defaultBranch) {
return {
success: false,
message:
'Could not determine default branch. Make sure remote is configured.'
};
}
return {
success: true,
value: defaultBranch,
message: `Default branch: ${defaultBranch}`
};
} catch (error: any) {
return {
success: false,
message: `Failed to detect default branch: ${error.message}`
};
}
}
/**
* Run all preflight checks
*/
async runAllChecks(): Promise<PreflightResult> {
logger.info('Running preflight checks...');
const testCommand = await this.detectTestCommand();
const gitWorkingTree = await this.checkGitWorkingTree();
const requiredTools = await this.validateRequiredTools();
const defaultBranch = await this.detectDefaultBranch();
const allSuccess =
testCommand.success &&
gitWorkingTree.success &&
requiredTools.success &&
defaultBranch.success;
// Build summary
const passed: string[] = [];
const failed: string[] = [];
if (testCommand.success) passed.push('Test command');
else failed.push('Test command');
if (gitWorkingTree.success) passed.push('Git working tree');
else failed.push('Git working tree');
if (requiredTools.success) passed.push('Required tools');
else failed.push('Required tools');
if (defaultBranch.success) passed.push('Default branch');
else failed.push('Default branch');
const total = passed.length + failed.length;
const summary = allSuccess
? `All preflight checks passed (${passed.length}/${total})`
: `Preflight checks failed: ${failed.join(', ')} (${passed.length}/${total} passed)`;
logger.info(summary);
return {
success: allSuccess,
testCommand,
gitWorkingTree,
requiredTools,
defaultBranch,
summary
};
}
}
```
--------------------------------------------------------------------------------
/docs/examples/codex-cli-usage.md:
--------------------------------------------------------------------------------
```markdown
# Codex CLI Provider Usage Examples
This guide provides practical examples of using Task Master with the Codex CLI provider.
## Prerequisites
Before using these examples, ensure you have:
```bash
# 1. Codex CLI installed
npm install -g @openai/codex
# 2. Authenticated with ChatGPT
codex login
# 3. Codex CLI configured as your provider
task-master models --set-main gpt-5-codex --codex-cli
```
## Example 1: Basic Task Creation
Use Codex CLI to create tasks from a simple description:
```bash
# Add a task with AI-powered enhancement
task-master add-task --prompt="Implement user authentication with JWT" --research
```
**What happens**:
1. Task Master sends your prompt to GPT-5-Codex via the CLI
2. The AI analyzes your request and generates a detailed task
3. The task is added to your `.taskmaster/tasks/tasks.json`
4. OAuth credentials are automatically used (no API key needed)
## Example 2: Parsing a Product Requirements Document
Create a comprehensive task list from a PRD:
```bash
# Create your PRD
cat > my-feature.txt <<EOF
# User Profile Feature
## Requirements
1. Users can view their profile
2. Users can edit their information
3. Profile pictures can be uploaded
4. Email verification required
## Technical Constraints
- Use React for frontend
- Node.js/Express backend
- PostgreSQL database
EOF
# Parse with Codex CLI
task-master parse-prd my-feature.txt --num-tasks 12
```
**What happens**:
1. GPT-5-Codex reads and analyzes your PRD
2. Generates structured tasks with dependencies
3. Creates subtasks for complex items
4. Saves everything to `.taskmaster/tasks/`
## Example 3: Expanding Tasks with Research
Break down a complex task into detailed subtasks:
```bash
# First, show your current tasks
task-master list
# Expand a specific task (e.g., task 1.2)
task-master expand --id=1.2 --research --force
```
**What happens**:
1. Codex CLI uses GPT-5 for research-level analysis
2. Breaks down the task into logical subtasks
3. Adds implementation details and test strategies
4. Updates the task with dependency information
## Example 4: Analyzing Project Complexity
Get AI-powered insights into your project's task complexity:
```bash
# Analyze all tasks
task-master analyze-complexity --research
# View the complexity report
task-master complexity-report
```
**What happens**:
1. GPT-5 analyzes each task's scope and requirements
2. Assigns complexity scores and estimates subtask counts
3. Generates a detailed report
4. Saves to `.taskmaster/reports/task-complexity-report.json`
## Example 5: Using Custom Codex CLI Settings
Configure Codex CLI behavior for different commands:
```json
// In .taskmaster/config.json
{
"models": {
"main": {
"provider": "codex-cli",
"modelId": "gpt-5-codex",
"maxTokens": 128000,
"temperature": 0.2
}
},
"codexCli": {
"allowNpx": true,
"approvalMode": "on-failure",
"sandboxMode": "workspace-write",
"commandSpecific": {
"parse-prd": {
"verbose": true,
"approvalMode": "never"
},
"expand": {
"sandboxMode": "read-only",
"verbose": true
}
}
}
}
```
```bash
# Now parse-prd runs with verbose output and no approvals
task-master parse-prd requirements.txt
# Expand runs with read-only mode
task-master expand --id=2.1
```
## Example 6: Workflow - Building a Feature End-to-End
Complete workflow from PRD to implementation tracking:
```bash
# Step 1: Initialize project
task-master init
# Step 2: Set up Codex CLI
task-master models --set-main gpt-5-codex --codex-cli
task-master models --set-fallback gpt-5 --codex-cli
# Step 3: Create PRD
cat > feature-prd.txt <<EOF
# Authentication System
Implement a complete authentication system with:
- User registration
- Email verification
- Password reset
- Two-factor authentication
- Session management
EOF
# Step 4: Parse PRD into tasks
task-master parse-prd feature-prd.txt --num-tasks 8
# Step 5: Analyze complexity
task-master analyze-complexity --research
# Step 6: Expand complex tasks
task-master expand --all --research
# Step 7: Start working
task-master next
# Shows: Task 1.1: User registration database schema
# Step 8: Mark completed as you work
task-master set-status --id=1.1 --status=done
# Step 9: Continue to next task
task-master next
```
## Example 7: Multi-Role Configuration
Use Codex CLI for main tasks, Perplexity for research:
```json
// In .taskmaster/config.json
{
"models": {
"main": {
"provider": "codex-cli",
"modelId": "gpt-5-codex",
"maxTokens": 128000,
"temperature": 0.2
},
"research": {
"provider": "perplexity",
"modelId": "sonar-pro",
"maxTokens": 8700,
"temperature": 0.1
},
"fallback": {
"provider": "codex-cli",
"modelId": "gpt-5",
"maxTokens": 128000,
"temperature": 0.2
}
}
}
```
```bash
# Main task operations use GPT-5-Codex
task-master add-task --prompt="Build REST API endpoint"
# Research operations use Perplexity
task-master analyze-complexity --research
# Fallback to GPT-5 if needed
task-master expand --id=3.2 --force
```
## Example 8: Troubleshooting Common Issues
### Issue: Codex CLI not found
```bash
# Check if Codex is installed
codex --version
# If not found, install globally
npm install -g @openai/codex
# Or enable npx fallback in config
cat >> .taskmaster/config.json <<EOF
{
"codexCli": {
"allowNpx": true
}
}
EOF
```
### Issue: Not authenticated
```bash
# Check auth status
codex
# Use /about command to see auth info
# Re-authenticate if needed
codex login
```
### Issue: Want more verbose output
```bash
# Enable verbose mode in config
cat >> .taskmaster/config.json <<EOF
{
"codexCli": {
"verbose": true
}
}
EOF
# Or for specific commands
task-master parse-prd my-prd.txt
# (verbose output shows detailed Codex CLI interactions)
```
## Example 9: CI/CD Integration
Use Codex CLI in automated workflows:
```yaml
# .github/workflows/task-analysis.yml
name: Analyze Task Complexity
on:
push:
paths:
- '.taskmaster/**'
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Task Master
run: npm install -g task-master-ai
- name: Configure Codex CLI
run: |
npm install -g @openai/codex
echo "${{ secrets.OPENAI_CODEX_API_KEY }}" > ~/.codex-auth
env:
OPENAI_CODEX_API_KEY: ${{ secrets.OPENAI_CODEX_API_KEY }}
- name: Configure Task Master
run: |
cat > .taskmaster/config.json <<EOF
{
"models": {
"main": {
"provider": "codex-cli",
"modelId": "gpt-5"
}
},
"codexCli": {
"allowNpx": true,
"skipGitRepoCheck": true,
"approvalMode": "never",
"fullAuto": true
}
}
EOF
- name: Analyze Complexity
run: task-master analyze-complexity --research
- name: Upload Report
uses: actions/upload-artifact@v3
with:
name: complexity-report
path: .taskmaster/reports/task-complexity-report.json
```
## Best Practices
### 1. Use OAuth for Development
```bash
# For local development, use OAuth (no API key needed)
codex login
task-master models --set-main gpt-5-codex --codex-cli
```
### 2. Configure Approval Modes Appropriately
```json
{
"codexCli": {
"approvalMode": "on-failure", // Safe default
"sandboxMode": "workspace-write" // Restricts to project directory
}
}
```
### 3. Use Command-Specific Settings
```json
{
"codexCli": {
"commandSpecific": {
"parse-prd": {
"approvalMode": "never", // PRD parsing is safe
"verbose": true
},
"expand": {
"approvalMode": "on-request", // More cautious for task expansion
"verbose": false
}
}
}
}
```
### 4. Leverage Codebase Analysis
```json
{
"global": {
"enableCodebaseAnalysis": true // Let Codex analyze your code
}
}
```
### 5. Handle Errors Gracefully
```bash
# Always configure a fallback model
task-master models --set-fallback gpt-5 --codex-cli
# Or use a different provider as fallback
task-master models --set-fallback claude-3-5-sonnet
```
## Next Steps
- Read the [Codex CLI Provider Documentation](../providers/codex-cli.md)
- Explore [Configuration Options](../configuration.md#codex-cli-provider)
- Check out [Command Reference](../command-reference.md)
- Learn about [Task Structure](../task-structure.md)
## Common Patterns
### Pattern: Daily Development Workflow
```bash
# Morning: Review tasks
task-master list
# Get next task
task-master next
# Work on task...
# Update task with notes
task-master update-subtask --id=2.3 --prompt="Implemented authentication middleware"
# Mark complete
task-master set-status --id=2.3 --status=done
# Repeat
```
### Pattern: Feature Planning
```bash
# Write feature spec
vim new-feature.txt
# Generate tasks
task-master parse-prd new-feature.txt --num-tasks 10
# Analyze and expand
task-master analyze-complexity --research
task-master expand --all --research --force
# Review and adjust
task-master list
```
### Pattern: Sprint Planning
```bash
# Parse sprint requirements
task-master parse-prd sprint-requirements.txt
# Analyze complexity
task-master analyze-complexity --research
# View report
task-master complexity-report
# Adjust task estimates based on complexity scores
```
---
For more examples and advanced usage, see the [full documentation](https://docs.task-master.dev).
```
--------------------------------------------------------------------------------
/apps/cli/src/utils/auto-update.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Auto-update utilities for task-master-ai CLI
*/
import { spawn } from 'child_process';
import https from 'https';
import boxen from 'boxen';
import chalk from 'chalk';
import ora from 'ora';
import process from 'process';
export interface UpdateInfo {
currentVersion: string;
latestVersion: string;
needsUpdate: boolean;
highlights?: string[];
}
/**
* Get current version from build-time injected environment variable
*/
function getCurrentVersion(): string {
// Version is injected at build time via TM_PUBLIC_VERSION
const version = process.env.TM_PUBLIC_VERSION;
if (version && version !== 'unknown') {
return version;
}
// Fallback for development or if injection failed
console.warn('Could not read version from TM_PUBLIC_VERSION, using fallback');
return '0.0.0';
}
/**
* Compare semantic versions with proper pre-release handling
* @param v1 - First version
* @param v2 - Second version
* @returns -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
*/
export function compareVersions(v1: string, v2: string): number {
const toParts = (v: string) => {
const [core, pre = ''] = v.split('-', 2);
const nums = core.split('.').map((n) => Number.parseInt(n, 10) || 0);
return { nums, pre };
};
const a = toParts(v1);
const b = toParts(v2);
const len = Math.max(a.nums.length, b.nums.length);
// Compare numeric parts
for (let i = 0; i < len; i++) {
const d = (a.nums[i] || 0) - (b.nums[i] || 0);
if (d !== 0) return d < 0 ? -1 : 1;
}
// Handle pre-release comparison
if (a.pre && !b.pre) return -1; // prerelease < release
if (!a.pre && b.pre) return 1; // release > prerelease
if (a.pre === b.pre) return 0; // same or both empty
return a.pre < b.pre ? -1 : 1; // basic prerelease tie-break
}
/**
* Fetch CHANGELOG.md from GitHub and extract highlights for a specific version
*/
async function fetchChangelogHighlights(version: string): Promise<string[]> {
return new Promise((resolve) => {
const options = {
hostname: 'raw.githubusercontent.com',
path: '/eyaltoledano/claude-task-master/main/CHANGELOG.md',
method: 'GET',
headers: {
'User-Agent': `task-master-ai/${version}`
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
if (res.statusCode !== 200) {
resolve([]);
return;
}
const highlights = parseChangelogHighlights(data, version);
resolve(highlights);
} catch (error) {
resolve([]);
}
});
});
req.on('error', () => {
resolve([]);
});
req.setTimeout(3000, () => {
req.destroy();
resolve([]);
});
req.end();
});
}
/**
* Parse changelog markdown to extract Minor Changes for a specific version
* @internal - Exported for testing purposes only
*/
export function parseChangelogHighlights(
changelog: string,
version: string
): string[] {
try {
// Validate version format (basic semver pattern) to prevent ReDoS
if (!/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$/.test(version)) {
return [];
}
// Find the version section
const versionRegex = new RegExp(
`## ${version.replace(/\./g, '\\.')}\\s*\\n`,
'i'
);
const versionMatch = changelog.match(versionRegex);
if (!versionMatch) {
return [];
}
// Extract content from this version to the next version heading
const startIdx = versionMatch.index! + versionMatch[0].length;
const nextVersionIdx = changelog.indexOf('\n## ', startIdx);
const versionContent =
nextVersionIdx > 0
? changelog.slice(startIdx, nextVersionIdx)
: changelog.slice(startIdx);
// Find Minor Changes section
const minorChangesMatch = versionContent.match(
/### Minor Changes\s*\n([\s\S]*?)(?=\n###|\n##|$)/i
);
if (!minorChangesMatch) {
return [];
}
const minorChangesContent = minorChangesMatch[1];
const highlights: string[] = [];
// Extract all bullet points (lines starting with -)
// Format: - [#PR](...) Thanks [@author]! - Description
const bulletRegex = /^-\s+\[#\d+\][^\n]*?!\s+-\s+(.+?)$/gm;
let match;
while ((match = bulletRegex.exec(minorChangesContent)) !== null) {
const desc = match[1].trim();
highlights.push(desc);
}
return highlights;
} catch (error) {
return [];
}
}
/**
* Check for newer version of task-master-ai
*/
export async function checkForUpdate(
currentVersionOverride?: string
): Promise<UpdateInfo> {
const currentVersion = currentVersionOverride || getCurrentVersion();
return new Promise((resolve) => {
const options = {
hostname: 'registry.npmjs.org',
path: '/task-master-ai',
method: 'GET',
headers: {
Accept: 'application/vnd.npm.install-v1+json',
'User-Agent': `task-master-ai/${currentVersion}`
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', async () => {
try {
if (res.statusCode !== 200)
throw new Error(`npm registry status ${res.statusCode}`);
const npmData = JSON.parse(data);
const latestVersion = npmData['dist-tags']?.latest || currentVersion;
const needsUpdate =
compareVersions(currentVersion, latestVersion) < 0;
// Fetch highlights if update is needed
let highlights: string[] | undefined;
if (needsUpdate) {
highlights = await fetchChangelogHighlights(latestVersion);
}
resolve({
currentVersion,
latestVersion,
needsUpdate,
highlights
});
} catch (error) {
resolve({
currentVersion,
latestVersion: currentVersion,
needsUpdate: false
});
}
});
});
req.on('error', () => {
resolve({
currentVersion,
latestVersion: currentVersion,
needsUpdate: false
});
});
req.setTimeout(3000, () => {
req.destroy();
resolve({
currentVersion,
latestVersion: currentVersion,
needsUpdate: false
});
});
req.end();
});
}
/**
* Display upgrade notification message
*/
export function displayUpgradeNotification(
currentVersion: string,
latestVersion: string,
highlights?: string[]
) {
let content = `${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}`;
if (highlights && highlights.length > 0) {
content += '\n\n' + chalk.bold("What's New:");
for (const highlight of highlights) {
content += '\n' + chalk.cyan('• ') + highlight;
}
content += '\n\n' + 'Auto-updating to the latest version...';
} else {
content +=
'\n\n' +
'Auto-updating to the latest version with new features and bug fixes...';
}
const message = boxen(content, {
padding: 1,
margin: { top: 1, bottom: 1 },
borderColor: 'yellow',
borderStyle: 'round'
});
console.log(message);
}
/**
* Automatically update task-master-ai to the latest version
*/
export async function performAutoUpdate(
latestVersion: string
): Promise<boolean> {
if (
process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1' ||
process.env.CI ||
process.env.NODE_ENV === 'test'
) {
const reason =
process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1'
? 'TASKMASTER_SKIP_AUTO_UPDATE=1'
: process.env.CI
? 'CI environment'
: 'NODE_ENV=test';
console.log(chalk.dim(`Skipping auto-update (${reason})`));
return false;
}
const spinner = ora({
text: chalk.blue(
`Updating task-master-ai to version ${chalk.green(latestVersion)}`
),
spinner: 'dots',
color: 'blue'
}).start();
return new Promise((resolve) => {
const updateProcess = spawn(
'npm',
[
'install',
'-g',
`task-master-ai@${latestVersion}`,
'--no-fund',
'--no-audit',
'--loglevel=warn'
],
{
stdio: ['ignore', 'pipe', 'pipe']
}
);
let errorOutput = '';
updateProcess.stdout.on('data', () => {
// Update spinner text with progress
spinner.text = chalk.blue(
`Installing task-master-ai@${latestVersion}...`
);
});
updateProcess.stderr.on('data', (data) => {
errorOutput += data.toString();
});
updateProcess.on('close', (code) => {
if (code === 0) {
spinner.succeed(
chalk.green(
`Successfully updated to version ${chalk.bold(latestVersion)}`
)
);
resolve(true);
} else {
spinner.fail(chalk.red('Auto-update failed'));
console.log(
chalk.cyan(
`Please run manually: npm install -g task-master-ai@${latestVersion}`
)
);
if (errorOutput) {
console.log(chalk.dim(`Error: ${errorOutput.trim()}`));
}
resolve(false);
}
});
updateProcess.on('error', (error) => {
spinner.fail(chalk.red('Auto-update failed'));
console.log(chalk.red('Error:'), error.message);
console.log(
chalk.cyan(
`Please run manually: npm install -g task-master-ai@${latestVersion}`
)
);
resolve(false);
});
});
}
/**
* Restart the CLI with the newly installed version
* @param argv - Original command-line arguments (process.argv)
*/
export function restartWithNewVersion(argv: string[]): void {
const args = argv.slice(2); // Remove 'node' and script path
console.log(chalk.dim('Restarting with updated version...\n'));
// Spawn the updated task-master command
const child = spawn('task-master', args, {
stdio: 'inherit', // Inherit stdin/stdout/stderr so it looks seamless
detached: false,
shell: process.platform === 'win32' // Windows compatibility
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 0);
});
child.on('error', (error) => {
console.error(
chalk.red('Failed to restart with new version:'),
error.message
);
console.log(chalk.yellow('Please run your command again manually.'));
process.exit(1);
});
}
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/tasks/services/task-loader.service.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Task Loader Service
* Loads and validates tasks for autopilot execution
*/
import type { Task, Subtask } from '../../../common/types/index.js';
import type { TaskService } from './task-service.js';
import { getLogger } from '../../../common/logger/factory.js';
import { isTaskComplete } from '../../../common/constants/index.js';
const logger = getLogger('TaskLoader');
/**
* Validation error types
*/
export type ValidationErrorType =
| 'task_not_found'
| 'task_completed'
| 'no_subtasks'
| 'circular_dependencies'
| 'missing_dependencies'
| 'invalid_structure';
/**
* Validation result for task loading
*/
export interface TaskValidationResult {
/** Whether validation passed */
success: boolean;
/** Loaded task (only present if validation succeeded) */
task?: Task;
/** Error type */
errorType?: ValidationErrorType;
/** Human-readable error message */
errorMessage?: string;
/** Actionable suggestion for fixing the error */
suggestion?: string;
/** Dependency analysis (only for dependency errors) */
dependencyIssues?: DependencyIssue[];
}
/**
* Dependency issue details
*/
export interface DependencyIssue {
/** Subtask ID with the issue */
subtaskId: string;
/** Type of dependency issue */
issueType: 'circular' | 'missing' | 'invalid';
/** Description of the issue */
message: string;
/** The problematic dependency reference */
dependencyRef?: string;
}
/**
* TaskLoaderService loads and validates tasks for autopilot execution
*/
export class TaskLoaderService {
private taskService: TaskService;
constructor(taskService: TaskService) {
if (!taskService) {
throw new Error('taskService is required for TaskLoaderService');
}
this.taskService = taskService;
}
/**
* Load and validate a task for autopilot execution
*/
async loadAndValidateTask(taskId: string): Promise<TaskValidationResult> {
logger.info(`Loading task ${taskId}...`);
// Step 1: Load task
const task = await this.loadTask(taskId);
if (!task) {
return {
success: false,
errorType: 'task_not_found',
errorMessage: `Task with ID "${taskId}" not found`,
suggestion:
'Use "task-master list" to see available tasks or verify the task ID is correct.'
};
}
// Step 2: Validate task status
const statusValidation = this.validateTaskStatus(task);
if (!statusValidation.success) {
return statusValidation;
}
// Step 3: Check for subtasks
const subtaskValidation = this.validateSubtasksExist(task);
if (!subtaskValidation.success) {
return subtaskValidation;
}
// Step 4: Validate subtask structure
const structureValidation = this.validateSubtaskStructure(task);
if (!structureValidation.success) {
return structureValidation;
}
// Step 5: Analyze dependencies
const dependencyValidation = this.validateDependencies(task);
if (!dependencyValidation.success) {
return dependencyValidation;
}
logger.info(`Task ${taskId} validated successfully`);
return {
success: true,
task
};
}
/**
* Load task using TaskService
*/
private async loadTask(taskId: string): Promise<Task | null> {
try {
return await this.taskService.getTask(taskId);
} catch (error) {
logger.error(`Failed to load task ${taskId}:`, error);
return null;
}
}
/**
* Validate task status is appropriate for autopilot
*/
private validateTaskStatus(task: Task): TaskValidationResult {
if (isTaskComplete(task.status)) {
return {
success: false,
errorType: 'task_completed',
errorMessage: `Task "${task.title}" is already ${task.status}`,
suggestion:
'Autopilot can only execute tasks that are pending or in-progress. Use a different task.'
};
}
return { success: true };
}
/**
* Validate task has subtasks
*/
private validateSubtasksExist(task: Task): TaskValidationResult {
if (!task.subtasks || task.subtasks.length === 0) {
return {
success: false,
errorType: 'no_subtasks',
errorMessage: `Task "${task.title}" has no subtasks`,
suggestion: this.buildExpansionSuggestion(task)
};
}
return { success: true };
}
/**
* Build helpful suggestion for expanding tasks
*/
private buildExpansionSuggestion(task: Task): string {
const suggestions: string[] = [
`Autopilot requires tasks to be broken down into subtasks for execution.`
];
// Add expansion command suggestion
suggestions.push(`\nExpand this task using:`);
suggestions.push(` task-master expand --id=${task.id}`);
// If task has complexity analysis, mention it
if (task.complexity || task.recommendedSubtasks) {
suggestions.push(
`\nThis task has complexity analysis available. Consider reviewing it first:`
);
suggestions.push(` task-master show ${task.id}`);
} else {
suggestions.push(
`\nOr analyze task complexity first to determine optimal subtask count:`
);
suggestions.push(` task-master analyze-complexity --from=${task.id}`);
}
return suggestions.join('\n');
}
/**
* Validate subtask structure
*/
private validateSubtaskStructure(task: Task): TaskValidationResult {
for (const subtask of task.subtasks) {
// Check required fields
if (!subtask.title || !subtask.description) {
return {
success: false,
errorType: 'invalid_structure',
errorMessage: `Subtask ${task.id}.${subtask.id} is missing required fields`,
suggestion:
'Subtasks must have title and description. Re-expand the task or manually fix the subtask structure.'
};
}
// Validate dependencies are arrays
if (subtask.dependencies && !Array.isArray(subtask.dependencies)) {
return {
success: false,
errorType: 'invalid_structure',
errorMessage: `Subtask ${task.id}.${subtask.id} has invalid dependencies format`,
suggestion:
'Dependencies must be an array. Fix the task structure manually.'
};
}
}
return { success: true };
}
/**
* Validate subtask dependencies
*/
private validateDependencies(task: Task): TaskValidationResult {
const issues: DependencyIssue[] = [];
const subtaskIds = new Set(task.subtasks.map((st) => String(st.id)));
for (const subtask of task.subtasks) {
const subtaskId = `${task.id}.${subtask.id}`;
// Check for missing dependencies
if (subtask.dependencies && subtask.dependencies.length > 0) {
for (const depId of subtask.dependencies) {
const depIdStr = String(depId);
if (!subtaskIds.has(depIdStr)) {
issues.push({
subtaskId,
issueType: 'missing',
message: `References non-existent subtask ${depIdStr}`,
dependencyRef: depIdStr
});
}
}
}
// Check for circular dependencies
const circularCheck = this.detectCircularDependency(
subtask,
task.subtasks,
new Set()
);
if (circularCheck) {
issues.push({
subtaskId,
issueType: 'circular',
message: `Circular dependency detected: ${circularCheck.join(' -> ')}`
});
}
}
if (issues.length > 0) {
const errorType =
issues[0].issueType === 'circular'
? 'circular_dependencies'
: 'missing_dependencies';
return {
success: false,
errorType,
errorMessage: `Task "${task.title}" has dependency issues`,
suggestion:
'Fix dependency issues manually or re-expand the task:\n' +
issues
.map((issue) => ` - ${issue.subtaskId}: ${issue.message}`)
.join('\n'),
dependencyIssues: issues
};
}
return { success: true };
}
/**
* Detect circular dependencies using depth-first search
*/
private detectCircularDependency(
subtask: Subtask,
allSubtasks: Subtask[],
visited: Set<string>
): string[] | null {
const subtaskId = String(subtask.id);
if (visited.has(subtaskId)) {
return [subtaskId];
}
visited.add(subtaskId);
if (subtask.dependencies && subtask.dependencies.length > 0) {
for (const depId of subtask.dependencies) {
const depIdStr = String(depId);
const dependency = allSubtasks.find((st) => String(st.id) === depIdStr);
if (dependency) {
const circular = this.detectCircularDependency(
dependency,
allSubtasks,
new Set(visited)
);
if (circular) {
return [subtaskId, ...circular];
}
}
}
}
return null;
}
/**
* Get ordered subtask execution sequence
* Returns subtasks in dependency order (tasks with no deps first)
*/
getExecutionOrder(task: Task): Subtask[] {
const ordered: Subtask[] = [];
const completed = new Set<string>();
// Keep adding subtasks whose dependencies are all completed
while (ordered.length < task.subtasks.length) {
let added = false;
for (const subtask of task.subtasks) {
const subtaskId = String(subtask.id);
if (completed.has(subtaskId)) {
continue;
}
// Check if all dependencies are completed
const allDepsCompleted =
!subtask.dependencies ||
subtask.dependencies.length === 0 ||
subtask.dependencies.every((depId) => completed.has(String(depId)));
if (allDepsCompleted) {
ordered.push(subtask);
completed.add(subtaskId);
added = true;
break;
}
}
// Safety check to prevent infinite loop
if (!added && ordered.length < task.subtasks.length) {
logger.warn(
`Could not determine complete execution order for task ${task.id}`
);
// Add remaining subtasks in original order
for (const subtask of task.subtasks) {
if (!completed.has(String(subtask.id))) {
ordered.push(subtask);
}
}
break;
}
}
return ordered;
}
/**
* Clean up resources
*/
async cleanup(): Promise<void> {
// TaskService doesn't require explicit cleanup
// Resources are automatically released when instance is garbage collected
}
}
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/logger/logger.spec.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Tests for MCP logging integration
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { type LogCallback, LogLevel, Logger } from './logger.js';
describe('Logger - MCP Integration', () => {
// Store original environment
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
// Save original environment
originalEnv = {
MCP_MODE: process.env.MCP_MODE,
TASK_MASTER_MCP: process.env.TASK_MASTER_MCP,
TASK_MASTER_SILENT: process.env.TASK_MASTER_SILENT,
TM_SILENT: process.env.TM_SILENT,
TASK_MASTER_LOG_LEVEL: process.env.TASK_MASTER_LOG_LEVEL,
TM_LOG_LEVEL: process.env.TM_LOG_LEVEL,
NO_COLOR: process.env.NO_COLOR,
TASK_MASTER_NO_COLOR: process.env.TASK_MASTER_NO_COLOR
};
// Clear environment variables for clean tests
delete process.env.MCP_MODE;
delete process.env.TASK_MASTER_MCP;
delete process.env.TASK_MASTER_SILENT;
delete process.env.TM_SILENT;
delete process.env.TASK_MASTER_LOG_LEVEL;
delete process.env.TM_LOG_LEVEL;
delete process.env.NO_COLOR;
delete process.env.TASK_MASTER_NO_COLOR;
});
afterEach(() => {
// Restore original environment
for (const [key, value] of Object.entries(originalEnv)) {
if (value === undefined) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
});
describe('Callback-based logging', () => {
it('should call callback instead of console when logCallback is provided', () => {
const mockCallback = vi.fn();
const logger = new Logger({
level: LogLevel.INFO,
logCallback: mockCallback
});
logger.info('Test message');
expect(mockCallback).toHaveBeenCalledWith(
'info',
expect.stringContaining('Test message')
);
});
it('should call callback for all log levels', () => {
const mockCallback = vi.fn();
const logger = new Logger({
level: LogLevel.DEBUG,
logCallback: mockCallback
});
logger.error('Error message');
logger.warn('Warning message');
logger.info('Info message');
logger.debug('Debug message');
expect(mockCallback).toHaveBeenNthCalledWith(
1,
'error',
expect.stringContaining('Error message')
);
expect(mockCallback).toHaveBeenNthCalledWith(
2,
'warn',
expect.stringContaining('Warning message')
);
expect(mockCallback).toHaveBeenNthCalledWith(
3,
'info',
expect.stringContaining('Info message')
);
expect(mockCallback).toHaveBeenNthCalledWith(
4,
'debug',
expect.stringContaining('Debug message')
);
});
it('should respect log level with callback', () => {
const mockCallback = vi.fn();
const logger = new Logger({
level: LogLevel.WARN,
logCallback: mockCallback
});
logger.debug('Debug message');
logger.info('Info message');
logger.warn('Warning message');
logger.error('Error message');
// Only warn and error should be logged
expect(mockCallback).toHaveBeenCalledTimes(2);
expect(mockCallback).toHaveBeenNthCalledWith(
1,
'warn',
expect.stringContaining('Warning message')
);
expect(mockCallback).toHaveBeenNthCalledWith(
2,
'error',
expect.stringContaining('Error message')
);
});
it('should handle raw log() calls with callback', () => {
const mockCallback = vi.fn();
const logger = new Logger({
level: LogLevel.INFO,
logCallback: mockCallback
});
logger.log('Raw message', 'with args');
expect(mockCallback).toHaveBeenCalledWith('log', 'Raw message with args');
});
});
describe('MCP mode with callback', () => {
it('should not silence logs when mcpMode=true and callback is provided', () => {
const mockCallback = vi.fn();
const logger = new Logger({
level: LogLevel.INFO,
mcpMode: true,
logCallback: mockCallback
});
logger.info('Test message');
expect(mockCallback).toHaveBeenCalledWith(
'info',
expect.stringContaining('Test message')
);
});
it('should silence logs when mcpMode=true and no callback', () => {
const consoleSpy = vi.spyOn(console, 'log');
const logger = new Logger({
level: LogLevel.INFO,
mcpMode: true
// No callback
});
logger.info('Test message');
expect(consoleSpy).not.toHaveBeenCalled();
consoleSpy.mockRestore();
});
});
describe('Child loggers', () => {
it('should inherit callback from parent', () => {
const mockCallback = vi.fn();
const parent = new Logger({
level: LogLevel.INFO,
logCallback: mockCallback
});
const child = parent.child('child');
child.info('Child message');
expect(mockCallback).toHaveBeenCalledWith(
'info',
expect.stringContaining('[child]')
);
expect(mockCallback).toHaveBeenCalledWith(
'info',
expect.stringContaining('Child message')
);
});
it('should allow child to override callback', () => {
const parentCallback = vi.fn();
const childCallback = vi.fn();
const parent = new Logger({
level: LogLevel.INFO,
logCallback: parentCallback
});
const child = parent.child('child', {
logCallback: childCallback
});
parent.info('Parent message');
child.info('Child message');
expect(parentCallback).toHaveBeenCalledTimes(1);
expect(childCallback).toHaveBeenCalledTimes(1);
});
});
describe('Configuration updates', () => {
it('should allow updating logCallback via setConfig', () => {
const callback1 = vi.fn();
const callback2 = vi.fn();
const logger = new Logger({
level: LogLevel.INFO,
logCallback: callback1
});
logger.info('Message 1');
expect(callback1).toHaveBeenCalledTimes(1);
logger.setConfig({ logCallback: callback2 });
logger.info('Message 2');
expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
});
it('should maintain mcpMode behavior when updating config', () => {
const callback = vi.fn();
const logger = new Logger({
level: LogLevel.INFO,
mcpMode: true
});
// Initially silent (no callback)
logger.info('Message 1');
expect(callback).not.toHaveBeenCalled();
// Add callback - should start logging
logger.setConfig({ logCallback: callback });
logger.info('Message 2');
expect(callback).toHaveBeenCalledTimes(1);
});
});
describe('Formatting with callback', () => {
it('should include prefix in callback messages', () => {
const mockCallback = vi.fn();
const logger = new Logger({
level: LogLevel.INFO,
prefix: 'test-prefix',
logCallback: mockCallback
});
logger.info('Test message');
expect(mockCallback).toHaveBeenCalledWith(
'info',
expect.stringContaining('[test-prefix]')
);
});
it('should include timestamp when enabled', () => {
const mockCallback = vi.fn();
const logger = new Logger({
level: LogLevel.INFO,
timestamp: true,
logCallback: mockCallback
});
logger.info('Test message');
const [[, message]] = mockCallback.mock.calls;
// Message should contain ISO timestamp pattern
expect(message).toMatch(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
});
it('should format additional arguments', () => {
const mockCallback = vi.fn();
const logger = new Logger({
level: LogLevel.INFO,
logCallback: mockCallback
});
const data = { key: 'value' };
logger.info('Test message', data, 'string arg');
expect(mockCallback).toHaveBeenCalledWith(
'info',
expect.stringContaining('Test message')
);
expect(mockCallback).toHaveBeenCalledWith(
'info',
expect.stringContaining('"key"')
);
expect(mockCallback).toHaveBeenCalledWith(
'info',
expect.stringContaining('string arg')
);
});
});
describe('Edge cases', () => {
it('should handle null/undefined callback gracefully', () => {
const logger = new Logger({
level: LogLevel.INFO,
logCallback: undefined
});
const consoleSpy = vi.spyOn(console, 'log');
// Should fallback to console
logger.info('Test message');
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it('should not call callback when level is SILENT', () => {
const mockCallback = vi.fn();
const logger = new Logger({
level: LogLevel.SILENT,
logCallback: mockCallback
});
logger.error('Error');
logger.warn('Warning');
logger.info('Info');
logger.debug('Debug');
expect(mockCallback).not.toHaveBeenCalled();
});
it('should propagate callback errors', () => {
const errorCallback: LogCallback = () => {
throw new Error('Callback error');
};
const logger = new Logger({
level: LogLevel.INFO,
logCallback: errorCallback
});
// Should throw
expect(() => {
logger.info('Test message');
}).toThrow('Callback error');
});
});
describe('Environment variable detection', () => {
it('should detect MCP mode from environment', () => {
const originalEnv = process.env.MCP_MODE;
process.env.MCP_MODE = 'true';
const logger = new Logger({
level: LogLevel.INFO
});
const config = logger.getConfig();
expect(config.mcpMode).toBe(true);
expect(config.silent).toBe(true); // Should be silent without callback
// Cleanup
if (originalEnv === undefined) {
delete process.env.MCP_MODE;
} else {
process.env.MCP_MODE = originalEnv;
}
});
it('should detect log level from environment', () => {
const originalEnv = process.env.TASK_MASTER_LOG_LEVEL;
process.env.TASK_MASTER_LOG_LEVEL = 'DEBUG';
const logger = new Logger();
const config = logger.getConfig();
expect(config.level).toBe(LogLevel.DEBUG);
// Cleanup
if (originalEnv === undefined) {
delete process.env.TASK_MASTER_LOG_LEVEL;
} else {
process.env.TASK_MASTER_LOG_LEVEL = originalEnv;
}
});
});
});
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/services/config-persistence.service.spec.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Unit tests for ConfigPersistence service
*/
import fs from 'node:fs/promises';
import type { PartialConfiguration } from '@tm/core/common/interfaces/configuration.interface.js';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ConfigPersistence } from './config-persistence.service.js';
vi.mock('node:fs', () => ({
promises: {
readFile: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn(),
unlink: vi.fn(),
access: vi.fn(),
readdir: vi.fn(),
rename: vi.fn()
}
}));
describe('ConfigPersistence', () => {
let persistence: ConfigPersistence;
const testProjectRoot = '/test/project';
beforeEach(() => {
persistence = new ConfigPersistence(testProjectRoot);
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('saveConfig', () => {
const mockConfig: PartialConfiguration = {
models: { main: 'test-model', fallback: 'test-fallback' },
storage: {
type: 'file' as const,
enableBackup: true,
maxBackups: 5,
enableCompression: true,
encoding: 'utf-8',
atomicOperations: true
}
};
it('should save configuration to file', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
await persistence.saveConfig(mockConfig);
expect(fs.mkdir).toHaveBeenCalledWith('/test/project/.taskmaster', {
recursive: true
});
expect(fs.writeFile).toHaveBeenCalledWith(
'/test/project/.taskmaster/config.json',
JSON.stringify(mockConfig, null, 2),
'utf-8'
);
});
it('should use atomic write when specified', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.rename).mockResolvedValue(undefined);
await persistence.saveConfig(mockConfig, { atomic: true });
// Should write to temp file first
expect(fs.writeFile).toHaveBeenCalledWith(
'/test/project/.taskmaster/config.json.tmp',
JSON.stringify(mockConfig, null, 2),
'utf-8'
);
// Then rename to final location
expect(fs.rename).toHaveBeenCalledWith(
'/test/project/.taskmaster/config.json.tmp',
'/test/project/.taskmaster/config.json'
);
});
it('should create backup when requested', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.access).mockResolvedValue(undefined); // Config exists
vi.mocked(fs.readFile).mockResolvedValue('{"old": "config"}');
vi.mocked(fs.readdir).mockResolvedValue([]);
await persistence.saveConfig(mockConfig, { createBackup: true });
// Should create backup directory
expect(fs.mkdir).toHaveBeenCalledWith(
'/test/project/.taskmaster/backups',
{ recursive: true }
);
// Should read existing config for backup
expect(fs.readFile).toHaveBeenCalledWith(
'/test/project/.taskmaster/config.json',
'utf-8'
);
// Should write backup file
expect(fs.writeFile).toHaveBeenCalledWith(
expect.stringContaining('/test/project/.taskmaster/backups/config-'),
'{"old": "config"}',
'utf-8'
);
});
it('should not create backup if config does not exist', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
await persistence.saveConfig(mockConfig, { createBackup: true });
// Should not read or create backup
expect(fs.readFile).not.toHaveBeenCalled();
expect(fs.writeFile).toHaveBeenCalledTimes(1); // Only the main config
});
it('should throw TaskMasterError on save failure', async () => {
vi.mocked(fs.mkdir).mockRejectedValue(new Error('Disk full'));
await expect(persistence.saveConfig(mockConfig)).rejects.toThrow(
'Failed to save configuration'
);
});
});
describe('configExists', () => {
it('should return true when config exists', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
const exists = await persistence.configExists();
expect(fs.access).toHaveBeenCalledWith(
'/test/project/.taskmaster/config.json'
);
expect(exists).toBe(true);
});
it('should return false when config does not exist', async () => {
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
const exists = await persistence.configExists();
expect(exists).toBe(false);
});
});
describe('deleteConfig', () => {
it('should delete configuration file', async () => {
vi.mocked(fs.unlink).mockResolvedValue(undefined);
await persistence.deleteConfig();
expect(fs.unlink).toHaveBeenCalledWith(
'/test/project/.taskmaster/config.json'
);
});
it('should not throw when file does not exist', async () => {
const error = new Error('File not found') as any;
error.code = 'ENOENT';
vi.mocked(fs.unlink).mockRejectedValue(error);
await expect(persistence.deleteConfig()).resolves.not.toThrow();
});
it('should throw TaskMasterError for other errors', async () => {
vi.mocked(fs.unlink).mockRejectedValue(new Error('Permission denied'));
await expect(persistence.deleteConfig()).rejects.toThrow(
'Failed to delete configuration'
);
});
});
describe('getBackups', () => {
it('should return list of backup files sorted newest first', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
'config-2024-01-01T10-00-00-000Z.json',
'config-2024-01-02T10-00-00-000Z.json',
'config-2024-01-03T10-00-00-000Z.json',
'other-file.txt'
] as any);
const backups = await persistence.getBackups();
expect(fs.readdir).toHaveBeenCalledWith(
'/test/project/.taskmaster/backups'
);
expect(backups).toEqual([
'config-2024-01-03T10-00-00-000Z.json',
'config-2024-01-02T10-00-00-000Z.json',
'config-2024-01-01T10-00-00-000Z.json'
]);
});
it('should return empty array when backup directory does not exist', async () => {
vi.mocked(fs.readdir).mockRejectedValue(new Error('Not found'));
const backups = await persistence.getBackups();
expect(backups).toEqual([]);
});
it('should filter out non-backup files', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
'config-2024-01-01T10-00-00-000Z.json',
'README.md',
'.DS_Store',
'config.json',
'config-backup.json' // Wrong format
] as any);
const backups = await persistence.getBackups();
expect(backups).toEqual(['config-2024-01-01T10-00-00-000Z.json']);
});
});
describe('restoreFromBackup', () => {
const backupFile = 'config-2024-01-01T10-00-00-000Z.json';
const backupContent = '{"restored": "config"}';
it('should restore configuration from backup', async () => {
vi.mocked(fs.readFile).mockResolvedValue(backupContent);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
await persistence.restoreFromBackup(backupFile);
expect(fs.readFile).toHaveBeenCalledWith(
`/test/project/.taskmaster/backups/${backupFile}`,
'utf-8'
);
expect(fs.writeFile).toHaveBeenCalledWith(
'/test/project/.taskmaster/config.json',
backupContent,
'utf-8'
);
});
it('should throw TaskMasterError when backup file not found', async () => {
vi.mocked(fs.readFile).mockRejectedValue(new Error('File not found'));
await expect(
persistence.restoreFromBackup('nonexistent.json')
).rejects.toThrow('Failed to restore from backup');
});
it('should throw TaskMasterError on write failure', async () => {
vi.mocked(fs.readFile).mockResolvedValue(backupContent);
vi.mocked(fs.writeFile).mockRejectedValue(new Error('Disk full'));
await expect(persistence.restoreFromBackup(backupFile)).rejects.toThrow(
'Failed to restore from backup'
);
});
});
describe('backup management', () => {
it('should clean old backups when limit exceeded', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('{"old": "config"}');
vi.mocked(fs.unlink).mockResolvedValue(undefined);
// Mock 7 existing backups
vi.mocked(fs.readdir).mockResolvedValue([
'config-2024-01-01T10-00-00-000Z.json',
'config-2024-01-02T10-00-00-000Z.json',
'config-2024-01-03T10-00-00-000Z.json',
'config-2024-01-04T10-00-00-000Z.json',
'config-2024-01-05T10-00-00-000Z.json',
'config-2024-01-06T10-00-00-000Z.json',
'config-2024-01-07T10-00-00-000Z.json'
] as any);
await persistence.saveConfig({}, { createBackup: true });
// Should delete oldest backups (keeping 5)
expect(fs.unlink).toHaveBeenCalledWith(
'/test/project/.taskmaster/backups/config-2024-01-01T10-00-00-000Z.json'
);
expect(fs.unlink).toHaveBeenCalledWith(
'/test/project/.taskmaster/backups/config-2024-01-02T10-00-00-000Z.json'
);
});
it('should handle backup cleanup errors gracefully', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readFile).mockResolvedValue('{"old": "config"}');
vi.mocked(fs.readdir).mockResolvedValue(['config-old.json'] as any);
vi.mocked(fs.unlink).mockRejectedValue(new Error('Permission denied'));
// Mock console.warn to verify it's called
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
// Should not throw even if cleanup fails
await expect(
persistence.saveConfig({}, { createBackup: true })
).resolves.not.toThrow();
expect(warnSpy).toHaveBeenCalledWith(
'Failed to clean old backups:',
expect.any(Error)
);
warnSpy.mockRestore();
});
});
});
```
--------------------------------------------------------------------------------
/apps/cli/src/ui/components/task-detail.component.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Task detail component for show command
* Displays detailed task information in a structured format
*/
import type { StorageType, Subtask, Task } from '@tm/core';
import boxen from 'boxen';
import chalk from 'chalk';
import Table from 'cli-table3';
import { MarkedExtension, marked } from 'marked';
import { markedTerminal } from 'marked-terminal';
import {
getComplexityWithColor,
getPriorityWithColor,
getStatusWithColor
} from '../../utils/ui.js';
// Configure marked to use terminal renderer with subtle colors
marked.use(
markedTerminal({
// More subtle colors that match the overall design
code: (code: string) => {
// Custom code block handler to preserve formatting
return code
.split('\n')
.map((line) => ' ' + chalk.cyan(line))
.join('\n');
},
blockquote: chalk.gray.italic,
html: chalk.gray,
heading: chalk.white.bold, // White bold for headings
hr: chalk.gray,
listitem: chalk.white, // White for list items
paragraph: chalk.white, // White for paragraphs (default text color)
strong: chalk.white.bold, // White bold for strong text
em: chalk.white.italic, // White italic for emphasis
codespan: chalk.cyan, // Cyan for inline code (no background)
del: chalk.dim.strikethrough,
link: chalk.blue,
href: chalk.blue.underline,
// Add more explicit code block handling
showSectionPrefix: false,
unescape: true,
emoji: false,
// Try to preserve whitespace in code blocks
tab: 4,
width: 120
}) as MarkedExtension
);
// Also set marked options to preserve whitespace
marked.setOptions({
breaks: true,
gfm: true
});
/**
* Display the task header with tag
*/
export function displayTaskHeader(
taskId: string | number,
title: string
): void {
// Display task header box
console.log(
boxen(chalk.white.bold(`Task: #${taskId} - ${title}`), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'blue',
borderStyle: 'round'
})
);
}
/**
* Display task properties in a table format
*/
export function displayTaskProperties(
task: Task | Subtask,
originalTaskId?: string
): void {
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Create table for task properties - simple 2-column layout
const table = new Table({
head: [],
style: {
head: [],
border: ['grey']
},
colWidths: [
Math.floor(terminalWidth * 0.2),
Math.floor(terminalWidth * 0.8)
],
wordWrap: true
});
const deps =
task.dependencies && task.dependencies.length > 0
? task.dependencies.map((d) => String(d)).join(', ')
: 'None';
// Use originalTaskId if provided (for subtasks like "104.1")
const displayId = originalTaskId || String(task.id);
// Build the left column (labels) and right column (values)
const labels = [
chalk.cyan('ID:'),
chalk.cyan('Title:'),
chalk.cyan('Status:'),
chalk.cyan('Priority:'),
chalk.cyan('Dependencies:'),
chalk.cyan('Complexity:'),
chalk.cyan('Description:')
].join('\n');
const values = [
displayId,
task.title,
getStatusWithColor(task.status),
getPriorityWithColor(task.priority),
deps,
typeof task.complexity === 'number'
? getComplexityWithColor(task.complexity)
: chalk.gray('N/A'),
task.description || ''
].join('\n');
table.push([labels, values]);
console.log(table.toString());
}
/**
* Display implementation details in a box
*/
export function displayImplementationDetails(details: string): void {
// Handle all escaped characters properly
const cleanDetails = details
.replace(/\\n/g, '\n') // Convert \n to actual newlines
.replace(/\\t/g, '\t') // Convert \t to actual tabs
.replace(/\\"/g, '"') // Convert \" to actual quotes
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Parse markdown to terminal-friendly format
const markdownResult = marked(cleanDetails);
const formattedDetails =
typeof markdownResult === 'string' ? markdownResult.trim() : cleanDetails; // Fallback to original if Promise
console.log(
boxen(
chalk.white.bold('Implementation Details:') + '\n\n' + formattedDetails,
{
padding: 1,
borderStyle: 'round',
borderColor: 'cyan', // Changed to cyan to match the original
width: terminalWidth // Fixed width to match the original
}
)
);
}
/**
* Display test strategy in a box
*/
export function displayTestStrategy(testStrategy: string): void {
// Handle all escaped characters properly (same as implementation details)
const cleanStrategy = testStrategy
.replace(/\\n/g, '\n') // Convert \n to actual newlines
.replace(/\\t/g, '\t') // Convert \t to actual tabs
.replace(/\\"/g, '"') // Convert \" to actual quotes
.replace(/\\\\/g, '\\'); // Convert \\ to single backslash
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Parse markdown to terminal-friendly format (same as implementation details)
const markdownResult = marked(cleanStrategy);
const formattedStrategy =
typeof markdownResult === 'string' ? markdownResult.trim() : cleanStrategy; // Fallback to original if Promise
console.log(
boxen(chalk.white.bold('Test Strategy:') + '\n\n' + formattedStrategy, {
padding: 1,
borderStyle: 'round',
borderColor: 'cyan', // Changed to cyan to match implementation details
width: terminalWidth
})
);
}
/**
* Display subtasks in a table format
*/
export function displaySubtasks(
subtasks: Array<{
id: string | number;
title: string;
status: any;
description?: string;
dependencies?: string[];
}>,
parentTaskId?: string | number,
storageType?: Exclude<StorageType, 'auto'>
): void {
const terminalWidth = process.stdout.columns * 0.95 || 100;
// Display subtasks header
console.log(
boxen(chalk.magenta.bold('Subtasks'), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: 'magenta',
borderStyle: 'round',
margin: { top: 1, bottom: 0 }
})
);
// Create subtasks table
const table = new Table({
head: [
chalk.magenta.bold('ID'),
chalk.magenta.bold('Status'),
chalk.magenta.bold('Title'),
chalk.magenta.bold('Deps')
],
style: {
head: [],
border: ['grey']
},
colWidths: [
Math.floor(terminalWidth * 0.1),
Math.floor(terminalWidth * 0.15),
Math.floor(terminalWidth * 0.6),
Math.floor(terminalWidth * 0.15)
],
wordWrap: true
});
subtasks.forEach((subtask) => {
// Format subtask ID based on storage type:
// - File storage: Show parent prefix (e.g., 10.1, 10.2)
// - API storage: Show subtask ID only (e.g., 1, 2)
const subtaskId =
storageType === 'file' && parentTaskId
? `${parentTaskId}.${subtask.id}`
: String(subtask.id);
// Format dependencies
const deps =
subtask.dependencies && subtask.dependencies.length > 0
? subtask.dependencies.join(', ')
: 'None';
table.push([
subtaskId,
getStatusWithColor(subtask.status),
subtask.title,
deps
]);
});
console.log(table.toString());
}
/**
* Display suggested actions
*/
export function displaySuggestedActions(taskId: string | number): void {
console.log(
boxen(
chalk.white.bold('Suggested Actions:') +
'\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow(`task-master set-status --id=${taskId} --status=in-progress`)} to start working\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow(`task-master expand --id=${taskId}`)} to break down into subtasks\n` +
`${chalk.cyan('3.')} Run ${chalk.yellow(`task-master update-task --id=${taskId} --prompt="..."`)} to update details`,
{
padding: 1,
margin: { top: 1 },
borderStyle: 'round',
borderColor: 'green',
width: process.stdout.columns * 0.95 || 100
}
)
);
}
/**
* Display complete task details - used by both show and start commands
*/
export function displayTaskDetails(
task: Task | Subtask,
options?: {
statusFilter?: string;
showSuggestedActions?: boolean;
customHeader?: string;
headerColor?: string;
originalTaskId?: string;
storageType?: Exclude<StorageType, 'auto'>;
}
): void {
const {
statusFilter,
showSuggestedActions = false,
customHeader,
headerColor = 'blue',
originalTaskId,
storageType
} = options || {};
// Display header - either custom or default
if (customHeader) {
console.log(
boxen(chalk.white.bold(customHeader), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
borderColor: headerColor,
borderStyle: 'round',
margin: { top: 1 }
})
);
} else {
// Use originalTaskId if provided (for subtasks like "104.1")
const displayId = originalTaskId || task.id;
displayTaskHeader(displayId, task.title);
}
// Display task properties in table format
displayTaskProperties(task, originalTaskId);
// Display implementation details if available
if (task.details) {
console.log(); // Empty line for spacing
displayImplementationDetails(task.details);
}
// Display test strategy if available
if ('testStrategy' in task && task.testStrategy) {
console.log(); // Empty line for spacing
displayTestStrategy(task.testStrategy as string);
}
// Display subtasks if available
if (task.subtasks && task.subtasks.length > 0) {
// Filter subtasks by status if provided
const filteredSubtasks = statusFilter
? task.subtasks.filter((sub) => sub.status === statusFilter)
: task.subtasks;
if (filteredSubtasks.length === 0 && statusFilter) {
console.log(); // Empty line for spacing
console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
} else if (filteredSubtasks.length > 0) {
console.log(); // Empty line for spacing
displaySubtasks(filteredSubtasks, task.id, storageType);
}
}
// Display suggested actions if requested
if (showSuggestedActions) {
console.log(); // Empty line for spacing
const actionTaskId = originalTaskId || task.id;
displaySuggestedActions(actionTaskId);
}
}
```
--------------------------------------------------------------------------------
/src/utils/profiles.js:
--------------------------------------------------------------------------------
```javascript
/**
* Profiles Utility
* Consolidated utilities for profile detection, setup, and summary generation
*/
import fs from 'fs';
import path from 'path';
import inquirer from 'inquirer';
import chalk from 'chalk';
import boxen from 'boxen';
import { log } from '../../scripts/modules/utils.js';
import { getRulesProfile } from './rule-transformer.js';
import { RULE_PROFILES } from '../constants/profiles.js';
// =============================================================================
// PROFILE DETECTION
// =============================================================================
/**
* Get the display name for a profile
* @param {string} profileName - The profile name
* @returns {string} - The display name
*/
export function getProfileDisplayName(profileName) {
try {
const profile = getRulesProfile(profileName);
return profile.displayName || profileName;
} catch (error) {
return profileName;
}
}
/**
* Get installed profiles in the project directory
* @param {string} projectRoot - Project directory path
* @returns {string[]} - Array of installed profile names
*/
export function getInstalledProfiles(projectRoot) {
const installedProfiles = [];
for (const profileName of RULE_PROFILES) {
try {
const profile = getRulesProfile(profileName);
const profileDir = path.join(projectRoot, profile.profileDir);
// Check if profile directory exists (skip root directory check)
if (profile.profileDir === '.' || fs.existsSync(profileDir)) {
// Check if any files from the profile's fileMap exist
const rulesDir = path.join(projectRoot, profile.rulesDir);
if (fs.existsSync(rulesDir)) {
const ruleFiles = Object.values(profile.fileMap);
const hasRuleFiles = ruleFiles.some((ruleFile) =>
fs.existsSync(path.join(rulesDir, ruleFile))
);
if (hasRuleFiles) {
installedProfiles.push(profileName);
}
}
}
} catch (error) {
// Skip profiles that can't be loaded
}
}
return installedProfiles;
}
/**
* Check if removing specified profiles would leave no profiles installed
* @param {string} projectRoot - Project root directory
* @param {string[]} profilesToRemove - Array of profile names to remove
* @returns {boolean} - True if removal would leave no profiles
*/
export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) {
const installedProfiles = getInstalledProfiles(projectRoot);
// If no profiles are currently installed, removal cannot leave no profiles
if (installedProfiles.length === 0) {
return false;
}
const remainingProfiles = installedProfiles.filter(
(profile) => !profilesToRemove.includes(profile)
);
return remainingProfiles.length === 0;
}
// =============================================================================
// PROFILE SETUP
// =============================================================================
// Note: Profile choices are now generated dynamically within runInteractiveProfilesSetup()
// to ensure proper alphabetical sorting and pagination configuration
/**
* Launches an interactive prompt for selecting which rule profiles to include in your project.
*
* This function dynamically lists all available profiles (from RULE_PROFILES) and presents them as checkboxes.
* The user must select at least one profile (no defaults are pre-selected). The result is an array of selected profile names.
*
* Used by both project initialization (init) and the CLI 'task-master rules setup' command.
*
* @returns {Promise<string[]>} Array of selected profile names (e.g., ['cursor', 'windsurf'])
*/
export async function runInteractiveProfilesSetup() {
// Generate the profile list dynamically with proper display names, alphabetized
const profileDescriptions = RULE_PROFILES.map((profileName) => {
const displayName = getProfileDisplayName(profileName);
const profile = getRulesProfile(profileName);
// Determine description based on profile capabilities
let description;
const hasRules = Object.keys(profile.fileMap).length > 0;
const hasMcpConfig = profile.mcpConfig === true;
if (!profile.includeDefaultRules) {
// Integration guide profiles (claude, codex, gemini, opencode, zed, amp) - don't include standard coding rules
if (profileName === 'claude') {
description = 'Integration guide with Task Master slash commands';
} else if (profileName === 'codex') {
description = 'Comprehensive Task Master integration guide';
} else if (hasMcpConfig) {
description = 'Integration guide and MCP config';
} else {
description = 'Integration guide';
}
} else if (hasRules && hasMcpConfig) {
// Full rule profiles with MCP config
if (profileName === 'roo') {
description = 'Rule profile, MCP config, and agent modes';
} else {
description = 'Rule profile and MCP config';
}
} else if (hasRules) {
// Rule profiles without MCP config
description = 'Rule profile';
}
return {
profileName,
displayName,
description
};
}).sort((a, b) => a.displayName.localeCompare(b.displayName));
const profileListText = profileDescriptions
.map(
({ displayName, description }) =>
`${chalk.white('• ')}${chalk.yellow(displayName)}${chalk.white(` - ${description}`)}`
)
.join('\n');
console.log(
boxen(
`${chalk.white.bold('Rule Profiles Setup')}\n\n${chalk.white(
'Rule profiles help enforce best practices and conventions for Task Master.\n' +
'Each profile provides coding guidelines tailored for specific AI coding environments.\n\n'
)}${chalk.cyan('Available Profiles:')}\n${profileListText}`,
{
padding: 1,
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
// Generate choices in the same order as the display text above
const sortedChoices = profileDescriptions.map(
({ profileName, displayName }) => ({
name: displayName,
value: profileName
})
);
const ruleProfilesQuestion = {
type: 'checkbox',
name: 'ruleProfiles',
message: 'Which rule profiles would you like to add to your project?',
choices: sortedChoices,
pageSize: sortedChoices.length, // Show all options without pagination
loop: false, // Disable loop scrolling
validate: (input) => input.length > 0 || 'You must select at least one.'
};
const { ruleProfiles } = await inquirer.prompt([ruleProfilesQuestion]);
return ruleProfiles;
}
// =============================================================================
// PROFILE SUMMARY
// =============================================================================
/**
* Generate appropriate summary message for a profile based on its type
* @param {string} profileName - Name of the profile
* @param {Object} addResult - Result object with success/failed counts
* @returns {string} Formatted summary message
*/
export function generateProfileSummary(profileName, addResult) {
const profileConfig = getRulesProfile(profileName);
if (!profileConfig.includeDefaultRules) {
// Integration guide profiles (claude, codex, gemini, amp)
return `Summary for ${profileName}: Integration guide installed.`;
} else {
// Rule profiles with coding guidelines
return `Summary for ${profileName}: ${addResult.success} files processed, ${addResult.failed} failed.`;
}
}
/**
* Generate appropriate summary message for profile removal
* @param {string} profileName - Name of the profile
* @param {Object} removeResult - Result object from removal operation
* @returns {string} Formatted summary message
*/
export function generateProfileRemovalSummary(profileName, removeResult) {
if (removeResult.skipped) {
return `Summary for ${profileName}: Skipped (default or protected files)`;
}
if (removeResult.error && !removeResult.success) {
return `Summary for ${profileName}: Failed to remove - ${removeResult.error}`;
}
const profileConfig = getRulesProfile(profileName);
if (!profileConfig.includeDefaultRules) {
// Integration guide profiles (claude, codex, gemini, amp)
const baseMessage = `Summary for ${profileName}: Integration guide removed`;
if (removeResult.notice) {
return `${baseMessage} (${removeResult.notice})`;
}
return baseMessage;
} else {
// Rule profiles with coding guidelines
const baseMessage = `Summary for ${profileName}: Rule profile removed`;
if (removeResult.notice) {
return `${baseMessage} (${removeResult.notice})`;
}
return baseMessage;
}
}
/**
* Categorize profiles and generate final summary statistics
* @param {Array} addResults - Array of add result objects
* @returns {Object} Object with categorized profiles and totals
*/
export function categorizeProfileResults(addResults) {
const successfulProfiles = [];
let totalSuccess = 0;
let totalFailed = 0;
addResults.forEach((r) => {
totalSuccess += r.success;
totalFailed += r.failed;
// All profiles are considered successful if they completed without major errors
if (r.success > 0 || r.failed === 0) {
successfulProfiles.push(r.profileName);
}
});
return {
successfulProfiles,
allSuccessfulProfiles: successfulProfiles,
totalSuccess,
totalFailed
};
}
/**
* Categorize removal results and generate final summary statistics
* @param {Array} removalResults - Array of removal result objects
* @returns {Object} Object with categorized removal results
*/
export function categorizeRemovalResults(removalResults) {
const successfulRemovals = [];
const skippedRemovals = [];
const failedRemovals = [];
const removalsWithNotices = [];
removalResults.forEach((result) => {
if (result.success) {
successfulRemovals.push(result.profileName);
} else if (result.skipped) {
skippedRemovals.push(result.profileName);
} else if (result.error) {
failedRemovals.push(result);
}
if (result.notice) {
removalsWithNotices.push(result);
}
});
return {
successfulRemovals,
skippedRemovals,
failedRemovals,
removalsWithNotices
};
}
```
--------------------------------------------------------------------------------
/tests/e2e/e2e_helpers.sh:
--------------------------------------------------------------------------------
```bash
#!/bin/bash
# --- LLM Analysis Helper Function ---
# This function should be sourced by the main E2E script or test scripts.
# It requires curl and jq to be installed.
# It expects the project root path to be passed as the second argument.
# --- New Function: extract_and_sum_cost ---
# Takes a string containing command output.
# Extracts costs (lines with "Est. Cost: $X.YYYYYY" or similar from telemetry output)
# from the output, sums them, and adds them to the GLOBAL total_e2e_cost variable.
extract_and_sum_cost() {
local command_output="$1"
# Ensure total_e2e_cost is treated as a number, default to 0.0 if not set or invalid
if ! [[ "$total_e2e_cost" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
total_e2e_cost="0.0"
fi
local extracted_cost_sum="0.0"
# Grep for lines containing "Est. Cost: $", then extract the numeric value.
# Example line: │ Est. Cost: $0.093549 │
# Accumulate all costs found in the command_output
while IFS= read -r line; do
# Extract the numeric part after 'Est. Cost: $' and before any trailing spaces/chars
cost_value=$(echo "$line" | grep -o -E 'Est\. Cost: \$([0-9]+\.[0-9]+)' | sed -E 's/Est\. Cost: \$//g')
if [[ -n "$cost_value" && "$cost_value" =~ ^[0-9]+\.[0-9]+$ ]]; then
# echo "[DEBUG] Found cost value: $cost_value in line: '$line'" # For debugging
extracted_cost_sum=$(echo "$extracted_cost_sum + $cost_value" | bc)
# else # For debugging
# echo "[DEBUG] No valid cost value found or extracted in line: '$line' (extracted: '$cost_value')" # For debugging
fi
done < <(echo "$command_output" | grep -E 'Est\. Cost: \$')
# echo "[DEBUG] Extracted sum from this command output: $extracted_cost_sum" # For debugging
if (( $(echo "$extracted_cost_sum > 0" | bc -l) )); then
total_e2e_cost=$(echo "$total_e2e_cost + $extracted_cost_sum" | bc)
# echo "[DEBUG] Updated global total_e2e_cost: $total_e2e_cost" # For debugging
fi
# No echo here, the function modifies a global variable.
}
export -f extract_and_sum_cost # Export for use in other scripts if sourced
analyze_log_with_llm() {
local log_file="$1"
local project_root="$2" # Expect project root as the second argument
if [ -z "$project_root" ]; then
echo "[HELPER_ERROR] Project root argument is missing. Skipping LLM analysis." >&2
return 1
fi
local env_file="${project_root}/.env" # Path to .env in project root
local supported_models_file="${project_root}/scripts/modules/supported-models.json"
local provider_summary_log="provider_add_task_summary.log" # File summarizing provider test outcomes
local api_key=""
local api_endpoint="https://api.anthropic.com/v1/messages"
local api_key_name="ANTHROPIC_API_KEY"
local llm_analysis_model_id="claude-3-7-sonnet-20250219" # Model used for this analysis
local llm_analysis_provider="anthropic"
echo "" # Add a newline before analysis starts
if ! command -v jq &> /dev/null; then
echo "[HELPER_ERROR] LLM Analysis requires 'jq'. Skipping analysis." >&2
return 1
fi
if ! command -v curl &> /dev/null; then
echo "[HELPER_ERROR] LLM Analysis requires 'curl'. Skipping analysis." >&2
return 1
fi
if ! command -v bc &> /dev/null; then
echo "[HELPER_ERROR] LLM Analysis requires 'bc' for cost calculation. Skipping analysis." >&2
return 1
fi
if [ -f "$env_file" ]; then
api_key=$(grep "^${api_key_name}=" "$env_file" | sed -e "s/^${api_key_name}=//" -e 's/^[[:space:]"]*//' -e 's/[[:space:]"]*$//')
fi
if [ -z "$api_key" ]; then
echo "[HELPER_ERROR] ${api_key_name} not found or empty in project root .env file ($env_file). Skipping LLM analysis." >&2
return 1
fi
if [ ! -f "$log_file" ]; then
echo "[HELPER_ERROR] Log file not found: $log_file (PWD: $(pwd)). Check path passed to function. Skipping LLM analysis." >&2
return 1
fi
local log_content
log_content=$(cat "$log_file") || {
echo "[HELPER_ERROR] Failed to read log file: $log_file. Skipping LLM analysis." >&2
return 1
}
read -r -d '' prompt_template <<'EOF'
Analyze the following E2E test log for the task-master tool. The log contains output from various 'task-master' commands executed sequentially.
Your goal is to:
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).
2. **Specifically analyze the Multi-Provider Add-Task Test Sequence:**
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'.
b. For each tested provider, determine if `add-task` succeeded or failed. Note the created task ID if successful.
c. Review the corresponding `add_task_show_output_<provider>_id_<id>.log` file (if created) for each successful `add-task` execution.
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.
e. Note any providers where `add-task` failed or where the task ID could not be extracted.
3. Identify any general explicit "[ERROR]" messages or stack traces throughout the *entire* log.
4. Identify any potential warnings or unusual output that might indicate a problem even if not marked as an explicit error.
5. Provide an overall assessment of the test run's health based *only* on the log content.
Return your analysis **strictly** in the following JSON format. Do not include any text outside of the JSON structure:
{
"overall_status": "Success|Failure|Warning",
"verified_steps": [ "Initialization", "PRD Parsing", /* ...other general steps observed... */ ],
"provider_add_task_comparison": {
"prompt_used": "... (extract from log if possible or state 'standard auth prompt') ...",
"provider_results": {
"anthropic": { "status": "Success|Failure|ID_Extraction_Failed|Set_Model_Failed", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },
"openai": { "status": "Success|Failure|...", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },
/* ... include all tested providers ... */
},
"comparison_summary": "Brief overall comparison of generated tasks..."
},
"detected_issues": [ { "severity": "Error|Warning|Anomaly", "description": "...", "log_context": "[Optional, short snippet from log near the issue]" } ],
"llm_summary_points": [ "Overall summary point 1", "Provider comparison highlight", "Any major issues noted" ]
}
Here is the main log content:
%s
EOF
local full_prompt
if ! printf -v full_prompt "$prompt_template" "$log_content"; then
echo "[HELPER_ERROR] Failed to format prompt using printf." >&2
return 1
fi
local payload
payload=$(jq -n --arg prompt "$full_prompt" '{
"model": "'"$llm_analysis_model_id"'",
"max_tokens": 3072,
"messages": [
{"role": "user", "content": $prompt}
]
}') || {
echo "[HELPER_ERROR] Failed to create JSON payload using jq." >&2
return 1
}
local response_raw response_http_code response_body
response_raw=$(curl -s -w "\nHTTP_STATUS_CODE:%{http_code}" -X POST "$api_endpoint" \
-H "Content-Type: application/json" \
-H "x-api-key: $api_key" \
-H "anthropic-version: 2023-06-01" \
--data "$payload")
response_http_code=$(echo "$response_raw" | grep '^HTTP_STATUS_CODE:' | sed 's/HTTP_STATUS_CODE://')
response_body=$(echo "$response_raw" | sed '$d')
if [ "$response_http_code" != "200" ]; then
echo "[HELPER_ERROR] LLM API call failed with HTTP status $response_http_code." >&2
echo "[HELPER_ERROR] Response Body: $response_body" >&2
return 1
fi
if [ -z "$response_body" ]; then
echo "[HELPER_ERROR] LLM API call returned empty response body." >&2
return 1
fi
# Calculate cost of this LLM analysis call
local input_tokens output_tokens input_cost_per_1m output_cost_per_1m calculated_llm_cost
input_tokens=$(echo "$response_body" | jq -r '.usage.input_tokens // 0')
output_tokens=$(echo "$response_body" | jq -r '.usage.output_tokens // 0')
if [ -f "$supported_models_file" ]; then
model_cost_info=$(jq -r --arg provider "$llm_analysis_provider" --arg model_id "$llm_analysis_model_id" '
.[$provider][] | select(.id == $model_id) | .cost_per_1m_tokens
' "$supported_models_file")
if [[ -n "$model_cost_info" && "$model_cost_info" != "null" ]]; then
input_cost_per_1m=$(echo "$model_cost_info" | jq -r '.input // 0')
output_cost_per_1m=$(echo "$model_cost_info" | jq -r '.output // 0')
calculated_llm_cost=$(echo "($input_tokens / 1000000 * $input_cost_per_1m) + ($output_tokens / 1000000 * $output_cost_per_1m)" | bc -l)
# Format to 6 decimal places
formatted_llm_cost=$(printf "%.6f" "$calculated_llm_cost")
echo "LLM Analysis AI Cost: $formatted_llm_cost USD" # This line will be parsed by run_e2e.sh
else
echo "[HELPER_WARNING] Cost data for model $llm_analysis_model_id not found in $supported_models_file. LLM analysis cost not calculated."
fi
else
echo "[HELPER_WARNING] $supported_models_file not found. LLM analysis cost not calculated."
fi
# --- End cost calculation for this call ---
if echo "$response_body" | node "${project_root}/tests/e2e/parse_llm_output.cjs" "$log_file"; then
echo "[HELPER_SUCCESS] LLM analysis parsed and printed successfully by Node.js script."
return 0
else
local node_exit_code=$?
echo "[HELPER_ERROR] Node.js parsing script failed with exit code ${node_exit_code}."
echo "[HELPER_ERROR] Raw API response body (first 500 chars): $(echo "$response_body" | head -c 500)"
return 1
fi
}
export -f analyze_log_with_llm
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/utils/git-utils.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Git utilities for Task Master
* Git integration utilities using raw git commands and gh CLI
*/
import { exec, execSync } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* GitHub repository information
*/
export interface GitHubRepoInfo {
name: string;
owner: { login: string };
defaultBranchRef: { name: string };
}
/**
* Check if the specified directory is inside a git repository
*/
export async function isGitRepository(projectRoot: string): Promise<boolean> {
if (!projectRoot) {
throw new Error('projectRoot is required for isGitRepository');
}
try {
await execAsync('git rev-parse --git-dir', { cwd: projectRoot });
return true;
} catch (error) {
return false;
}
}
/**
* Synchronous check if directory is in a git repository
*/
export function isGitRepositorySync(projectRoot: string): boolean {
if (!projectRoot) {
return false;
}
try {
execSync('git rev-parse --git-dir', {
cwd: projectRoot,
stdio: 'ignore'
});
return true;
} catch (error) {
return false;
}
}
/**
* Get the current git branch name
*/
export async function getCurrentBranch(
projectRoot: string
): Promise<string | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for getCurrentBranch');
}
try {
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
cwd: projectRoot
});
return stdout.trim();
} catch (error) {
return null;
}
}
/**
* Synchronous get current git branch name
*/
export function getCurrentBranchSync(projectRoot: string): string | null {
if (!projectRoot) {
return null;
}
try {
const stdout = execSync('git rev-parse --abbrev-ref HEAD', {
cwd: projectRoot,
encoding: 'utf8'
});
return stdout.trim();
} catch (error) {
return null;
}
}
/**
* Get list of all local git branches
*/
export async function getLocalBranches(projectRoot: string): Promise<string[]> {
if (!projectRoot) {
throw new Error('projectRoot is required for getLocalBranches');
}
try {
const { stdout } = await execAsync(
'git branch --format="%(refname:short)"',
{ cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 }
);
return stdout
.trim()
.split('\n')
.filter((branch) => branch.length > 0)
.map((branch) => branch.trim());
} catch (error) {
return [];
}
}
/**
* Get list of all remote branches
*/
export async function getRemoteBranches(
projectRoot: string
): Promise<string[]> {
if (!projectRoot) {
throw new Error('projectRoot is required for getRemoteBranches');
}
try {
const { stdout } = await execAsync(
'git branch -r --format="%(refname:short)"',
{ cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 }
);
const names = stdout
.trim()
.split('\n')
.filter((branch) => branch.length > 0 && !branch.includes('HEAD'))
.map((branch) => branch.replace(/^[^/]+\//, '').trim());
return Array.from(new Set(names));
} catch (error) {
return [];
}
}
/**
* Check if gh CLI is available and authenticated
*/
export async function isGhCliAvailable(projectRoot?: string): Promise<boolean> {
try {
const options = projectRoot ? { cwd: projectRoot } : {};
await execAsync('gh auth status', options);
return true;
} catch (error) {
return false;
}
}
/**
* Get GitHub repository information using gh CLI
*/
export async function getGitHubRepoInfo(
projectRoot: string
): Promise<GitHubRepoInfo | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for getGitHubRepoInfo');
}
try {
const { stdout } = await execAsync(
'gh repo view --json name,owner,defaultBranchRef',
{ cwd: projectRoot }
);
return JSON.parse(stdout) as GitHubRepoInfo;
} catch (error) {
return null;
}
}
/**
* Get git repository root directory
*/
export async function getGitRepositoryRoot(
projectRoot: string
): Promise<string | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for getGitRepositoryRoot');
}
try {
const { stdout } = await execAsync('git rev-parse --show-toplevel', {
cwd: projectRoot
});
return stdout.trim();
} catch (error) {
return null;
}
}
/**
* Get the default branch name for the repository
*/
export async function getDefaultBranch(
projectRoot: string
): Promise<string | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for getDefaultBranch');
}
try {
// Try to get from GitHub first (if gh CLI is available)
if (await isGhCliAvailable(projectRoot)) {
const repoInfo = await getGitHubRepoInfo(projectRoot);
if (repoInfo && repoInfo.defaultBranchRef) {
return repoInfo.defaultBranchRef.name;
}
}
// Fallback to git remote info (support non-origin remotes)
const remotesRaw = await execAsync('git remote', { cwd: projectRoot });
const remotes = remotesRaw.stdout.trim().split('\n').filter(Boolean);
if (remotes.length > 0) {
const primary = remotes.includes('origin') ? 'origin' : remotes[0];
// Parse `git remote show` (preferred)
try {
const { stdout } = await execAsync(`git remote show ${primary}`, {
cwd: projectRoot,
maxBuffer: 10 * 1024 * 1024
});
const m = stdout.match(/HEAD branch:\s+([^\s]+)/);
if (m) return m[1].trim();
} catch {}
// Fallback to symbolic-ref of remote HEAD
try {
const { stdout } = await execAsync(
`git symbolic-ref refs/remotes/${primary}/HEAD`,
{ cwd: projectRoot }
);
return stdout.replace(`refs/remotes/${primary}/`, '').trim();
} catch {}
}
// If we couldn't determine, throw to trigger final fallbacks
throw new Error('default-branch-not-found');
} catch (error) {
// Final fallback - common default branch names
const commonDefaults = ['main', 'master'];
const branches = await getLocalBranches(projectRoot);
const remoteBranches = await getRemoteBranches(projectRoot);
for (const defaultName of commonDefaults) {
if (
branches.includes(defaultName) ||
remoteBranches.includes(defaultName)
) {
return defaultName;
}
}
return null;
}
}
/**
* Check if we're currently on the default branch
*/
export async function isOnDefaultBranch(projectRoot: string): Promise<boolean> {
if (!projectRoot) {
throw new Error('projectRoot is required for isOnDefaultBranch');
}
try {
const [currentBranch, defaultBranch] = await Promise.all([
getCurrentBranch(projectRoot),
getDefaultBranch(projectRoot)
]);
return (
currentBranch !== null &&
defaultBranch !== null &&
currentBranch === defaultBranch
);
} catch (error) {
return false;
}
}
/**
* Check if the current working directory is inside a Git work-tree
*/
export function insideGitWorkTree(): boolean {
try {
execSync('git rev-parse --is-inside-work-tree', {
stdio: 'ignore',
cwd: process.cwd()
});
return true;
} catch {
return false;
}
}
/**
* Sanitize branch name to be a valid tag name
*/
export function sanitizeBranchNameForTag(branchName: string): string {
if (!branchName || typeof branchName !== 'string') {
return 'unknown-branch';
}
// Replace invalid characters with hyphens and clean up
return branchName
.replace(/[^a-zA-Z0-9_.-]/g, '-') // Replace invalid chars with hyphens (allow dots)
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
.replace(/-+/g, '-') // Collapse multiple hyphens
.toLowerCase() // Convert to lowercase
.substring(0, 50); // Limit length
}
/**
* Check if a branch name would create a valid tag name
*/
export function isValidBranchForTag(branchName: string): boolean {
if (!branchName || typeof branchName !== 'string') {
return false;
}
// Check if it's a reserved branch name that shouldn't become tags
const reservedBranches = ['main', 'master', 'develop', 'dev', 'head'];
if (reservedBranches.includes(branchName.toLowerCase())) {
return false;
}
// Check if sanitized name would be meaningful
const sanitized = sanitizeBranchNameForTag(branchName);
return sanitized.length > 0 && sanitized !== 'unknown-branch';
}
/**
* Git worktree information
*/
export interface GitWorktree {
path: string;
branch: string | null;
head: string;
}
/**
* Get list of all git worktrees
*/
export async function getWorktrees(
projectRoot: string
): Promise<GitWorktree[]> {
if (!projectRoot) {
throw new Error('projectRoot is required for getWorktrees');
}
try {
const { stdout } = await execAsync('git worktree list --porcelain', {
cwd: projectRoot
});
const worktrees: GitWorktree[] = [];
const lines = stdout.trim().split('\n');
let current: Partial<GitWorktree> = {};
for (const line of lines) {
if (line.startsWith('worktree ')) {
// flush previous entry if present
if (current.path) {
worktrees.push({
path: current.path,
branch: current.branch || null,
head: current.head || ''
});
current = {};
}
current.path = line.substring(9);
} else if (line.startsWith('HEAD ')) {
current.head = line.substring(5);
} else if (line.startsWith('branch ')) {
current.branch = line.substring(7).replace('refs/heads/', '');
} else if (line === '' && current.path) {
worktrees.push({
path: current.path,
branch: current.branch || null,
head: current.head || ''
});
current = {};
}
}
// Handle last entry if no trailing newline
if (current.path) {
worktrees.push({
path: current.path,
branch: current.branch || null,
head: current.head || ''
});
}
return worktrees;
} catch (error) {
return [];
}
}
/**
* Check if a branch is checked out in any worktree
* Returns the worktree path if found, null otherwise
*/
export async function isBranchCheckedOut(
projectRoot: string,
branchName: string
): Promise<string | null> {
if (!projectRoot) {
throw new Error('projectRoot is required for isBranchCheckedOut');
}
if (!branchName) {
throw new Error('branchName is required for isBranchCheckedOut');
}
const worktrees = await getWorktrees(projectRoot);
const worktree = worktrees.find((wt) => wt.branch === branchName);
return worktree ? worktree.path : null;
}
```