This is page 29 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/tests/unit/profiles/amp-integration.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 | import fs from 'fs';
3 | import path from 'path';
4 | import { fileURLToPath } from 'url';
5 | import { getRulesProfile } from '../../../src/utils/rule-transformer.js';
6 |
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 |
10 | describe('Amp Profile Integration', () => {
11 | let tempDir;
12 | let ampProfile;
13 |
14 | beforeEach(() => {
15 | // Create temporary directory for testing
16 | tempDir = fs.mkdtempSync(path.join(__dirname, 'temp-amp-unit-'));
17 |
18 | // Get the Amp profile
19 | ampProfile = getRulesProfile('amp');
20 | });
21 |
22 | afterEach(() => {
23 | // Clean up temporary directory
24 | if (fs.existsSync(tempDir)) {
25 | fs.rmSync(tempDir, { recursive: true, force: true });
26 | }
27 | });
28 |
29 | describe('Profile Structure', () => {
30 | test('should have expected profile structure', () => {
31 | expect(ampProfile).toBeDefined();
32 | expect(ampProfile.profileName).toBe('amp');
33 | expect(ampProfile.displayName).toBe('Amp');
34 | expect(ampProfile.profileDir).toBe('.vscode');
35 | expect(ampProfile.rulesDir).toBe('.');
36 | expect(ampProfile.mcpConfig).toBe(true);
37 | expect(ampProfile.mcpConfigName).toBe('settings.json');
38 | expect(ampProfile.mcpConfigPath).toBe('.vscode/settings.json');
39 | expect(ampProfile.includeDefaultRules).toBe(false);
40 | });
41 |
42 | test('should have correct file mapping', () => {
43 | expect(ampProfile.fileMap).toEqual({
44 | 'AGENTS.md': '.taskmaster/AGENT.md'
45 | });
46 | });
47 |
48 | test('should not create unnecessary directories', () => {
49 | // Unlike profiles that copy entire directories, Amp should only create what's needed
50 | const assetsDir = path.join(tempDir, 'assets');
51 | fs.mkdirSync(assetsDir, { recursive: true });
52 | fs.writeFileSync(
53 | path.join(assetsDir, 'AGENTS.md'),
54 | 'Task Master instructions'
55 | );
56 |
57 | // Call onAddRulesProfile
58 | ampProfile.onAddRulesProfile(tempDir, assetsDir);
59 |
60 | // Should only have created .taskmaster directory and AGENT.md
61 | expect(fs.existsSync(path.join(tempDir, '.taskmaster'))).toBe(true);
62 | expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(true);
63 |
64 | // Should not have created any other directories (like .claude)
65 | expect(fs.existsSync(path.join(tempDir, '.amp'))).toBe(false);
66 | expect(fs.existsSync(path.join(tempDir, '.claude'))).toBe(false);
67 | });
68 | });
69 |
70 | describe('AGENT.md Import Logic', () => {
71 | test('should handle missing source file gracefully', () => {
72 | // Call onAddRulesProfile without creating source file
73 | const assetsDir = path.join(tempDir, 'assets');
74 | fs.mkdirSync(assetsDir, { recursive: true });
75 |
76 | // Should not throw error
77 | expect(() => {
78 | ampProfile.onAddRulesProfile(tempDir, assetsDir);
79 | }).not.toThrow();
80 |
81 | // Should not create any files
82 | expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(false);
83 | expect(fs.existsSync(path.join(tempDir, '.taskmaster', 'AGENT.md'))).toBe(
84 | false
85 | );
86 | });
87 |
88 | test('should preserve existing content when adding import', () => {
89 | // Create existing AGENT.md with specific content
90 | const existingContent =
91 | '# My Custom Amp Setup\n\nThis is my custom configuration.\n\n## Custom Section\n\nSome custom rules here.';
92 | fs.writeFileSync(path.join(tempDir, 'AGENT.md'), existingContent);
93 |
94 | // Create mock source
95 | const assetsDir = path.join(tempDir, 'assets');
96 | fs.mkdirSync(assetsDir, { recursive: true });
97 | fs.writeFileSync(
98 | path.join(assetsDir, 'AGENTS.md'),
99 | 'Task Master instructions'
100 | );
101 |
102 | // Call onAddRulesProfile
103 | ampProfile.onAddRulesProfile(tempDir, assetsDir);
104 |
105 | // Check that existing content is preserved
106 | const updatedContent = fs.readFileSync(
107 | path.join(tempDir, 'AGENT.md'),
108 | 'utf8'
109 | );
110 | expect(updatedContent).toContain('# My Custom Amp Setup');
111 | expect(updatedContent).toContain('This is my custom configuration.');
112 | expect(updatedContent).toContain('## Custom Section');
113 | expect(updatedContent).toContain('Some custom rules here.');
114 | expect(updatedContent).toContain('@./.taskmaster/AGENT.md');
115 | });
116 | });
117 |
118 | describe('MCP Configuration Handling', () => {
119 | test('should handle missing .vscode directory gracefully', () => {
120 | // Call onAddRulesProfile without .vscode directory
121 | const assetsDir = path.join(tempDir, 'assets');
122 | fs.mkdirSync(assetsDir, { recursive: true });
123 |
124 | // Should not throw error
125 | expect(() => {
126 | ampProfile.onAddRulesProfile(tempDir, assetsDir);
127 | }).not.toThrow();
128 | });
129 |
130 | test('should handle malformed JSON gracefully', () => {
131 | // Create .vscode directory with malformed JSON
132 | const vscodeDirPath = path.join(tempDir, '.vscode');
133 | fs.mkdirSync(vscodeDirPath, { recursive: true });
134 | fs.writeFileSync(
135 | path.join(vscodeDirPath, 'settings.json'),
136 | '{ malformed json'
137 | );
138 |
139 | // Should not throw error
140 | expect(() => {
141 | ampProfile.onAddRulesProfile(tempDir, path.join(tempDir, 'assets'));
142 | }).not.toThrow();
143 | });
144 |
145 | test('should preserve other VS Code settings when renaming', () => {
146 | // Create .vscode/settings.json with various settings
147 | const vscodeDirPath = path.join(tempDir, '.vscode');
148 | fs.mkdirSync(vscodeDirPath, { recursive: true });
149 |
150 | const initialConfig = {
151 | 'editor.fontSize': 14,
152 | 'editor.tabSize': 2,
153 | mcpServers: {
154 | 'task-master-ai': {
155 | command: 'npx',
156 | args: ['-y', 'task-master-ai']
157 | }
158 | },
159 | 'workbench.colorTheme': 'Dark+'
160 | };
161 |
162 | fs.writeFileSync(
163 | path.join(vscodeDirPath, 'settings.json'),
164 | JSON.stringify(initialConfig, null, '\t')
165 | );
166 |
167 | // Call onPostConvertRulesProfile (which handles MCP transformation)
168 | ampProfile.onPostConvertRulesProfile(
169 | tempDir,
170 | path.join(tempDir, 'assets')
171 | );
172 |
173 | // Check that other settings are preserved
174 | const settingsFile = path.join(vscodeDirPath, 'settings.json');
175 | const content = fs.readFileSync(settingsFile, 'utf8');
176 | const config = JSON.parse(content);
177 |
178 | expect(config['editor.fontSize']).toBe(14);
179 | expect(config['editor.tabSize']).toBe(2);
180 | expect(config['workbench.colorTheme']).toBe('Dark+');
181 | expect(config['amp.mcpServers']).toBeDefined();
182 | expect(config.mcpServers).toBeUndefined();
183 | });
184 | });
185 |
186 | describe('Removal Logic', () => {
187 | test('should handle missing files gracefully during removal', () => {
188 | // Should not throw error when removing non-existent files
189 | expect(() => {
190 | ampProfile.onRemoveRulesProfile(tempDir);
191 | }).not.toThrow();
192 | });
193 |
194 | test('should handle malformed JSON gracefully during removal', () => {
195 | // Create .vscode directory with malformed JSON
196 | const vscodeDirPath = path.join(tempDir, '.vscode');
197 | fs.mkdirSync(vscodeDirPath, { recursive: true });
198 | fs.writeFileSync(
199 | path.join(vscodeDirPath, 'settings.json'),
200 | '{ malformed json'
201 | );
202 |
203 | // Should not throw error
204 | expect(() => {
205 | ampProfile.onRemoveRulesProfile(tempDir);
206 | }).not.toThrow();
207 | });
208 |
209 | test('should preserve .vscode directory if it contains other files', () => {
210 | // Create .vscode directory with amp.mcpServers and other files
211 | const vscodeDirPath = path.join(tempDir, '.vscode');
212 | fs.mkdirSync(vscodeDirPath, { recursive: true });
213 |
214 | const initialConfig = {
215 | 'amp.mcpServers': {
216 | 'task-master-ai': {
217 | command: 'npx',
218 | args: ['-y', 'task-master-ai']
219 | }
220 | }
221 | };
222 |
223 | fs.writeFileSync(
224 | path.join(vscodeDirPath, 'settings.json'),
225 | JSON.stringify(initialConfig, null, '\t')
226 | );
227 |
228 | // Create another file in .vscode
229 | fs.writeFileSync(path.join(vscodeDirPath, 'launch.json'), '{}');
230 |
231 | // Call onRemoveRulesProfile
232 | ampProfile.onRemoveRulesProfile(tempDir);
233 |
234 | // Check that .vscode directory is preserved
235 | expect(fs.existsSync(vscodeDirPath)).toBe(true);
236 | expect(fs.existsSync(path.join(vscodeDirPath, 'launch.json'))).toBe(true);
237 | });
238 | });
239 |
240 | describe('Lifecycle Function Integration', () => {
241 | test('should have all required lifecycle functions', () => {
242 | expect(typeof ampProfile.onAddRulesProfile).toBe('function');
243 | expect(typeof ampProfile.onRemoveRulesProfile).toBe('function');
244 | expect(typeof ampProfile.onPostConvertRulesProfile).toBe('function');
245 | });
246 |
247 | test('onPostConvertRulesProfile should behave like onAddRulesProfile', () => {
248 | // Create mock source
249 | const assetsDir = path.join(tempDir, 'assets');
250 | fs.mkdirSync(assetsDir, { recursive: true });
251 | fs.writeFileSync(
252 | path.join(assetsDir, 'AGENTS.md'),
253 | 'Task Master instructions'
254 | );
255 |
256 | // Call onPostConvertRulesProfile
257 | ampProfile.onPostConvertRulesProfile(tempDir, assetsDir);
258 |
259 | // Should have same result as onAddRulesProfile
260 | expect(fs.existsSync(path.join(tempDir, '.taskmaster', 'AGENT.md'))).toBe(
261 | true
262 | );
263 | expect(fs.existsSync(path.join(tempDir, 'AGENT.md'))).toBe(true);
264 |
265 | const agentContent = fs.readFileSync(
266 | path.join(tempDir, 'AGENT.md'),
267 | 'utf8'
268 | );
269 | expect(agentContent).toContain('@./.taskmaster/AGENT.md');
270 | });
271 | });
272 |
273 | describe('Error Handling', () => {
274 | test('should handle file system errors gracefully', () => {
275 | // Mock fs.writeFileSync to throw an error
276 | const originalWriteFileSync = fs.writeFileSync;
277 | fs.writeFileSync = jest.fn().mockImplementation(() => {
278 | throw new Error('Permission denied');
279 | });
280 |
281 | // Create mock source
282 | const assetsDir = path.join(tempDir, 'assets');
283 | fs.mkdirSync(assetsDir, { recursive: true });
284 | originalWriteFileSync.call(
285 | fs,
286 | path.join(assetsDir, 'AGENTS.md'),
287 | 'Task Master instructions'
288 | );
289 |
290 | // Should not throw error
291 | expect(() => {
292 | ampProfile.onAddRulesProfile(tempDir, assetsDir);
293 | }).not.toThrow();
294 |
295 | // Restore original function
296 | fs.writeFileSync = originalWriteFileSync;
297 | });
298 | });
299 | });
300 |
```
--------------------------------------------------------------------------------
/docs/mcp-provider.md:
--------------------------------------------------------------------------------
```markdown
1 | # MCP Provider Implementation
2 |
3 | ## Overview
4 |
5 | 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.
6 |
7 | ## Architecture
8 |
9 | ### Components
10 |
11 | 1. **MCPProvider** (`mcp-server/src/providers/mcp-provider.js`)
12 | - Main provider class following Claude Code pattern
13 | - Session-based provider (no API key required)
14 | - Registers with provider registry on MCP server connect
15 |
16 | 2. **AI SDK Implementation** (`mcp-server/src/custom-sdk/`)
17 | - `index.js` - Provider factory function
18 | - `language-model.js` - LanguageModelV1 implementation with **doGenerateObject support**
19 | - `message-converter.js` - Format conversion utilities
20 | - `json-extractor.js` - **NEW**: Robust JSON extraction from AI responses
21 | - `schema-converter.js` - **NEW**: Schema-to-instructions conversion utility
22 | - `errors.js` - Error handling and mapping
23 |
24 | 3. **Integration Points**
25 | - MCP Server registration (`mcp-server/src/index.js`)
26 | - AI Services integration (`scripts/modules/ai-services-unified.js`)
27 | - Model configuration (`scripts/modules/supported-models.json`)
28 |
29 | ### Session Flow
30 |
31 | ```
32 | MCP Client Connect → MCP Server → registerRemoteProvider()
33 | ↓
34 | MCPRemoteProvider (existing)
35 | MCPProvider
36 | ↓
37 | Provider Registry
38 | ↓
39 | AI Services Layer
40 | ↓
41 | Text Generation + Object Generation
42 | ```
43 |
44 | ## Implementation Details
45 |
46 | ### Provider Registration
47 |
48 | The MCP server registers **both** providers when a client connects:
49 |
50 | ```javascript
51 | // mcp-server/src/index.js
52 | registerRemoteProvider(session) {
53 | if (session?.clientCapabilities?.sampling) {
54 | // Register existing provider
55 | // Register unified MCP provider
56 | const mcpProvider = new MCPProvider();
57 | mcpProvider.setSession(session);
58 |
59 | const providerRegistry = ProviderRegistry.getInstance();
60 | providerRegistry.registerProvider('mcp', mcpProvider);
61 | }
62 | }
63 | ```
64 |
65 | ### AI Services Integration
66 |
67 | The AI services layer includes the new provider:
68 |
69 | ```javascript
70 | // scripts/modules/ai-services-unified.js
71 | const PROVIDERS = {
72 | // ... other providers
73 | 'mcp': () => {
74 | const providerRegistry = ProviderRegistry.getInstance();
75 | return providerRegistry.getProvider('mcp');
76 | }
77 | };
78 | ```
79 |
80 | ### Message Conversion
81 |
82 | The provider converts between AI SDK and MCP formats:
83 |
84 | ```javascript
85 | // AI SDK prompt → MCP sampling format
86 | const { messages, systemPrompt } = convertToMCPFormat(options.prompt);
87 |
88 | // MCP response → AI SDK format
89 | const result = convertFromMCPFormat(response);
90 | ```
91 |
92 | ## Structured Object Generation (generateObject)
93 |
94 | ### Overview
95 |
96 | 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.
97 |
98 | ### Architecture
99 |
100 | The generateObject implementation includes:
101 |
102 | 1. **Schema-to-Instructions Conversion** (`schema-converter.js`)
103 | - Converts Zod schemas to natural language instructions
104 | - Generates example outputs to guide AI responses
105 | - Handles complex nested schemas and validation requirements
106 |
107 | 2. **JSON Extraction Pipeline** (`json-extractor.js`)
108 | - Multiple extraction strategies for robust JSON parsing
109 | - Handles code blocks, malformed JSON, and various response formats
110 | - Fallback mechanisms for maximum reliability
111 |
112 | 3. **Validation System**
113 | - Complete schema validation using Zod
114 | - Detailed error reporting for failed validations
115 | - Type-safe object generation
116 |
117 | ### Implementation Details
118 |
119 | #### doGenerateObject Method
120 |
121 | The `MCPLanguageModel` class implements the AI SDK's `doGenerateObject` method:
122 |
123 | ```javascript
124 | async doGenerateObject({ schema, objectName, prompt, ...options }) {
125 | // Convert schema to instructions
126 | const instructions = convertSchemaToInstructions(schema, objectName);
127 |
128 | // Enhance prompt with structured output requirements
129 | const enhancedPrompt = enhancePromptForObjectGeneration(prompt, instructions);
130 |
131 | // Generate response via MCP sampling
132 | const response = await this.doGenerate({ prompt: enhancedPrompt, ...options });
133 |
134 | // Extract and validate JSON
135 | const extractedJson = extractJsonFromResponse(response.text);
136 | const validatedObject = schema.parse(extractedJson);
137 |
138 | return {
139 | object: validatedObject,
140 | usage: response.usage,
141 | finishReason: response.finishReason
142 | };
143 | }
144 | ```
145 |
146 | #### AI SDK Compatibility
147 |
148 | The provider includes required properties for AI SDK object generation:
149 |
150 | ```javascript
151 | class MCPLanguageModel {
152 | get defaultObjectGenerationMode() {
153 | return 'tool';
154 | }
155 |
156 | get supportsStructuredOutputs() {
157 | return true;
158 | }
159 |
160 | // ... doGenerateObject implementation
161 | }
162 | ```
163 |
164 | ### Usage Examples
165 |
166 | #### PRD Parsing
167 |
168 | ```javascript
169 | import { z } from 'zod';
170 |
171 | const taskSchema = z.object({
172 | title: z.string(),
173 | description: z.string(),
174 | priority: z.enum(['high', 'medium', 'low']),
175 | dependencies: z.array(z.number()).optional()
176 | });
177 |
178 | const result = await generateObject({
179 | model: mcpModel,
180 | schema: taskSchema,
181 | prompt: 'Parse this PRD section into a task: [PRD content]'
182 | });
183 |
184 | console.log(result.object); // Validated task object
185 | ```
186 |
187 | #### Task Creation
188 |
189 | ```javascript
190 | const taskCreationSchema = z.object({
191 | task: z.object({
192 | title: z.string(),
193 | description: z.string(),
194 | details: z.string(),
195 | testStrategy: z.string(),
196 | priority: z.enum(['high', 'medium', 'low']),
197 | dependencies: z.array(z.number()).optional()
198 | })
199 | });
200 |
201 | const result = await generateObject({
202 | model: mcpModel,
203 | schema: taskCreationSchema,
204 | prompt: 'Create a comprehensive task for implementing user authentication'
205 | });
206 | ```
207 |
208 | ### Error Handling
209 |
210 | The implementation provides comprehensive error handling:
211 |
212 | - **Schema Validation Errors**: Detailed Zod validation messages
213 | - **JSON Extraction Failures**: Fallback strategies and clear error reporting
214 | - **MCP Communication Errors**: Proper error mapping and recovery
215 | - **Timeout Handling**: Configurable timeouts for long-running operations
216 |
217 | ### Testing
218 |
219 | The generateObject functionality is fully tested:
220 |
221 | ```bash
222 | # Test object generation
223 | npm test -- --grep "generateObject"
224 |
225 | # Test with actual MCP session
226 | node test-object-generation.js
227 | ```
228 |
229 | ### Supported Features
230 |
231 | ✅ **Schema Conversion**: Zod schemas → Natural language instructions
232 | ✅ **JSON Extraction**: Multiple strategies for robust parsing
233 | ✅ **Validation**: Complete schema validation with error reporting
234 | ✅ **Error Recovery**: Fallback mechanisms for failed extractions
235 | ✅ **Type Safety**: Full TypeScript support with inferred types
236 | ✅ **AI SDK Compliance**: Complete LanguageModelV1 interface implementation
237 |
238 | ## Usage
239 |
240 | ### Configuration
241 |
242 | Add to supported models configuration:
243 |
244 | ```json
245 | {
246 | "mcp": [
247 | {
248 | "id": "claude-3-5-sonnet-20241022",
249 | "swe_score": 0.623,
250 | "cost_per_1m_tokens": { "input": 0, "output": 0 },
251 | "allowed_roles": ["main", "fallback", "research"],
252 | "max_tokens": 200000
253 | }
254 | ]
255 | }
256 | ```
257 |
258 | ### CLI Usage
259 |
260 | ```bash
261 | # Set provider for main role
262 | tm models set-main --provider mcp --model claude-3-5-sonnet-20241022
263 |
264 | # Use in task operations
265 | tm add-task "Create user authentication system"
266 | ```
267 |
268 | ### Programmatic Usage
269 |
270 | ```javascript
271 | const provider = registry.getProvider('mcp');
272 | if (provider && provider.hasValidSession()) {
273 | const client = provider.getClient({ temperature: 0.7 });
274 | const model = client({ modelId: 'claude-3-5-sonnet-20241022' });
275 |
276 | const result = await model.doGenerate({
277 | prompt: [
278 | { role: 'user', content: 'Hello!' }
279 | ]
280 | });
281 | }
282 | ```
283 |
284 | ## Testing
285 |
286 | ### Component Tests
287 |
288 | ```bash
289 | # Test individual components
290 | node test-mcp-components.js
291 | ```
292 |
293 | ### Integration Testing
294 |
295 | 1. Start MCP server
296 | 2. Connect Claude client
297 | 3. Verify both providers are registered
298 | 4. Test AI operations through mcp provider
299 |
300 | ### Validation Checklist
301 |
302 | - ✅ Provider creation and initialization
303 | - ✅ Registry integration
304 | - ✅ Session management
305 | - ✅ Message conversion
306 | - ✅ Error handling
307 | - ✅ AI Services integration
308 | - ✅ Model configuration
309 |
310 | ## Key Benefits
311 |
312 | 1. **AI SDK Compliance** - Full LanguageModelV1 implementation
313 | 2. **Session Integration** - Leverages existing MCP session infrastructure
314 | 3. **Registry Pattern** - Uses provider registry for discovery
315 | 4. **Backward Compatibility** - Coexists with existing MCPRemoteProvider
316 | 5. **Future Ready** - Supports AI SDK features and patterns
317 |
318 | ## Troubleshooting
319 |
320 | ### Provider Not Found
321 |
322 | ```
323 | Error: Provider "mcp" not found in registry
324 | ```
325 |
326 | **Solution**: Ensure MCP server is running and client is connected
327 |
328 | ### Session Errors
329 |
330 | ```
331 | Error: MCP Provider requires active MCP session
332 | ```
333 |
334 | **Solution**: Check MCP client connection and session capabilities
335 |
336 | ### Sampling Errors
337 |
338 | ```
339 | Error: MCP session must have client sampling capabilities
340 | ```
341 |
342 | **Solution**: Verify MCP client supports sampling operations
343 |
344 | ## Next Steps
345 |
346 | 1. **Performance Optimization** - Add caching and connection pooling
347 | 2. **Enhanced Streaming** - Implement native streaming if MCP supports it
348 | 3. **Tool Integration** - Add support for function calling through MCP tools
349 | 4. **Monitoring** - Add metrics and logging for provider usage
350 | 5. **Documentation** - Update user guides and API documentation
351 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer-vscode.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mock fs module before importing anything that uses it
4 | jest.mock('fs', () => ({
5 | readFileSync: jest.fn(),
6 | writeFileSync: jest.fn(),
7 | existsSync: jest.fn(),
8 | mkdirSync: jest.fn()
9 | }));
10 |
11 | // Import modules after mocking
12 | import fs from 'fs';
13 | import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
14 | import { vscodeProfile } from '../../../src/profiles/vscode.js';
15 |
16 | describe('VS Code Rule Transformer', () => {
17 | // Set up spies on the mocked modules
18 | const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
19 | const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
20 | const mockExistsSync = jest.spyOn(fs, 'existsSync');
21 | const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
22 | const mockConsoleError = jest
23 | .spyOn(console, 'error')
24 | .mockImplementation(() => {});
25 |
26 | beforeEach(() => {
27 | jest.clearAllMocks();
28 | // Setup default mocks
29 | mockReadFileSync.mockReturnValue('');
30 | mockWriteFileSync.mockImplementation(() => {});
31 | mockExistsSync.mockReturnValue(true);
32 | mockMkdirSync.mockImplementation(() => {});
33 | });
34 |
35 | afterAll(() => {
36 | jest.restoreAllMocks();
37 | });
38 |
39 | it('should correctly convert basic terms', () => {
40 | const testContent = `---
41 | description: Test Cursor rule for basic terms
42 | globs: **/*
43 | alwaysApply: true
44 | ---
45 |
46 | This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
47 | Also has references to .mdc files and cursor rules.`;
48 |
49 | // Mock file read to return our test content
50 | mockReadFileSync.mockReturnValue(testContent);
51 |
52 | // Call the actual function
53 | const result = convertRuleToProfileRule(
54 | 'source.mdc',
55 | 'target.md',
56 | vscodeProfile
57 | );
58 |
59 | // Verify the function succeeded
60 | expect(result).toBe(true);
61 |
62 | // Verify file operations were called correctly
63 | expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8');
64 | expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
65 |
66 | // Get the transformed content that was written
67 | const writeCall = mockWriteFileSync.mock.calls[0];
68 | const transformedContent = writeCall[1];
69 |
70 | // Verify transformations
71 | expect(transformedContent).toContain('VS Code');
72 | expect(transformedContent).toContain('code.visualstudio.com');
73 | expect(transformedContent).toContain('.md');
74 | expect(transformedContent).toContain('vscode rules'); // "cursor rules" -> "vscode rules"
75 | expect(transformedContent).toContain('applyTo: "**/*"'); // globs -> applyTo transformation
76 | expect(transformedContent).not.toContain('cursor.so');
77 | expect(transformedContent).not.toContain('Cursor rule');
78 | expect(transformedContent).not.toContain('globs:');
79 | expect(transformedContent).not.toContain('alwaysApply:');
80 | });
81 |
82 | it('should correctly convert tool references', () => {
83 | const testContent = `---
84 | description: Test Cursor rule for tool references
85 | globs: **/*
86 | alwaysApply: true
87 | ---
88 |
89 | - Use the search tool to find code
90 | - The edit_file tool lets you modify files
91 | - run_command executes terminal commands
92 | - use_mcp connects to external services`;
93 |
94 | // Mock file read to return our test content
95 | mockReadFileSync.mockReturnValue(testContent);
96 |
97 | // Call the actual function
98 | const result = convertRuleToProfileRule(
99 | 'source.mdc',
100 | 'target.md',
101 | vscodeProfile
102 | );
103 |
104 | // Verify the function succeeded
105 | expect(result).toBe(true);
106 |
107 | // Get the transformed content that was written
108 | const writeCall = mockWriteFileSync.mock.calls[0];
109 | const transformedContent = writeCall[1];
110 |
111 | // Verify transformations (VS Code uses standard tool names, so no transformation)
112 | expect(transformedContent).toContain('search tool');
113 | expect(transformedContent).toContain('edit_file tool');
114 | expect(transformedContent).toContain('run_command');
115 | expect(transformedContent).toContain('use_mcp');
116 | expect(transformedContent).toContain('applyTo: "**/*"'); // globs -> applyTo transformation
117 | });
118 |
119 | it('should correctly update file references and directory paths', () => {
120 | const testContent = `---
121 | description: Test Cursor rule for file references
122 | globs: .cursor/rules/*.md
123 | alwaysApply: true
124 | ---
125 |
126 | This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
127 | [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).
128 | Files are in the .cursor/rules directory and we should reference the rules directory.`;
129 |
130 | // Mock file read to return our test content
131 | mockReadFileSync.mockReturnValue(testContent);
132 |
133 | // Call the actual function
134 | const result = convertRuleToProfileRule(
135 | 'source.mdc',
136 | 'target.instructions.md',
137 | vscodeProfile
138 | );
139 |
140 | // Verify the function succeeded
141 | expect(result).toBe(true);
142 |
143 | // Get the transformed content that was written
144 | const writeCall = mockWriteFileSync.mock.calls[0];
145 | const transformedContent = writeCall[1];
146 |
147 | // Verify transformations specific to VS Code
148 | expect(transformedContent).toContain(
149 | 'applyTo: ".github/instructions/*.md"'
150 | ); // globs -> applyTo with path transformation
151 | expect(transformedContent).toContain(
152 | '(.github/instructions/dev_workflow.instructions.md)'
153 | ); // File path transformation - no taskmaster subdirectory for VS Code
154 | expect(transformedContent).toContain(
155 | '(.github/instructions/taskmaster.instructions.md)'
156 | ); // File path transformation - no taskmaster subdirectory for VS Code
157 | expect(transformedContent).toContain('instructions directory'); // "rules directory" -> "instructions directory"
158 | expect(transformedContent).not.toContain('(mdc:.cursor/rules/');
159 | expect(transformedContent).not.toContain('.cursor/rules');
160 | expect(transformedContent).not.toContain('globs:');
161 | expect(transformedContent).not.toContain('rules directory');
162 | });
163 |
164 | it('should transform globs to applyTo with various patterns', () => {
165 | const testContent = `---
166 | description: Test VS Code applyTo transformation
167 | globs: .cursor/rules/*.md
168 | alwaysApply: true
169 | ---
170 |
171 | Another section:
172 | globs: **/*.ts
173 | final: true
174 |
175 | Last one:
176 | globs: src/**/*
177 | ---`;
178 |
179 | // Mock file read to return our test content
180 | mockReadFileSync.mockReturnValue(testContent);
181 |
182 | // Call the actual function
183 | const result = convertRuleToProfileRule(
184 | 'source.mdc',
185 | 'target.md',
186 | vscodeProfile
187 | );
188 |
189 | // Verify the function succeeded
190 | expect(result).toBe(true);
191 |
192 | // Get the transformed content that was written
193 | const writeCall = mockWriteFileSync.mock.calls[0];
194 | const transformedContent = writeCall[1];
195 |
196 | // Verify all globs transformations
197 | expect(transformedContent).toContain(
198 | 'applyTo: ".github/instructions/*.md"'
199 | ); // Path transformation applied
200 | expect(transformedContent).toContain('applyTo: "**/*.ts"'); // Pattern with quotes
201 | expect(transformedContent).toContain('applyTo: "src/**/*"'); // Complex pattern with quotes
202 | expect(transformedContent).not.toContain('globs:'); // No globs should remain
203 | });
204 |
205 | it('should handle VS Code MCP configuration paths correctly', () => {
206 | const testContent = `---
207 | description: Test MCP configuration paths
208 | globs: **/*
209 | alwaysApply: true
210 | ---
211 |
212 | MCP configuration is at .cursor/mcp.json for Cursor.
213 | The .cursor/rules directory contains rules.
214 | Update your .cursor/mcp.json file accordingly.`;
215 |
216 | // Mock file read to return our test content
217 | mockReadFileSync.mockReturnValue(testContent);
218 |
219 | // Call the actual function
220 | const result = convertRuleToProfileRule(
221 | 'source.mdc',
222 | 'target.md',
223 | vscodeProfile
224 | );
225 |
226 | // Verify the function succeeded
227 | expect(result).toBe(true);
228 |
229 | // Get the transformed content that was written
230 | const writeCall = mockWriteFileSync.mock.calls[0];
231 | const transformedContent = writeCall[1];
232 |
233 | // Verify MCP paths are correctly transformed
234 | expect(transformedContent).toContain('.vscode/mcp.json'); // MCP config in .vscode
235 | expect(transformedContent).toContain('.github/instructions'); // Rules/instructions in .github/instructions
236 | expect(transformedContent).not.toContain('.cursor/mcp.json');
237 | expect(transformedContent).not.toContain('.cursor/rules');
238 | });
239 |
240 | it('should handle file read errors', () => {
241 | // Mock file read to throw an error
242 | mockReadFileSync.mockImplementation(() => {
243 | throw new Error('File not found');
244 | });
245 |
246 | // Call the actual function
247 | const result = convertRuleToProfileRule(
248 | 'nonexistent.mdc',
249 | 'target.md',
250 | vscodeProfile
251 | );
252 |
253 | // Verify the function failed gracefully
254 | expect(result).toBe(false);
255 |
256 | // Verify writeFileSync was not called
257 | expect(mockWriteFileSync).not.toHaveBeenCalled();
258 |
259 | // Verify error was logged
260 | expect(mockConsoleError).toHaveBeenCalledWith(
261 | 'Error converting rule file: File not found'
262 | );
263 | });
264 |
265 | it('should handle file write errors', () => {
266 | const testContent = 'test content';
267 | mockReadFileSync.mockReturnValue(testContent);
268 |
269 | // Mock file write to throw an error
270 | mockWriteFileSync.mockImplementation(() => {
271 | throw new Error('Permission denied');
272 | });
273 |
274 | // Call the actual function
275 | const result = convertRuleToProfileRule(
276 | 'source.mdc',
277 | 'target.md',
278 | vscodeProfile
279 | );
280 |
281 | // Verify the function failed gracefully
282 | expect(result).toBe(false);
283 |
284 | // Verify error was logged
285 | expect(mockConsoleError).toHaveBeenCalledWith(
286 | 'Error converting rule file: Permission denied'
287 | );
288 | });
289 |
290 | it('should create target directory if it does not exist', () => {
291 | const testContent = 'test content';
292 | mockReadFileSync.mockReturnValue(testContent);
293 |
294 | // Mock directory doesn't exist initially
295 | mockExistsSync.mockReturnValue(false);
296 |
297 | // Call the actual function
298 | convertRuleToProfileRule(
299 | 'source.mdc',
300 | '.github/instructions/deep/path/target.md',
301 | vscodeProfile
302 | );
303 |
304 | // Verify directory creation was called
305 | expect(mockMkdirSync).toHaveBeenCalledWith(
306 | '.github/instructions/deep/path',
307 | {
308 | recursive: true
309 | }
310 | );
311 | });
312 | });
313 |
```
--------------------------------------------------------------------------------
/apps/cli/src/commands/briefs.command.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Briefs Command - Friendly alias for tag management in API storage
3 | * Provides brief-specific commands that only work with API storage
4 | */
5 |
6 | import {
7 | type LogLevel,
8 | type TagInfo,
9 | tryAddTagViaRemote,
10 | tryListTagsViaRemote
11 | } from '@tm/bridge';
12 | import type { TmCore } from '@tm/core';
13 | import { AuthManager, createTmCore } from '@tm/core';
14 | import { Command } from 'commander';
15 | import { checkAuthentication } from '../utils/auth-helpers.js';
16 | import {
17 | selectBriefFromInput,
18 | selectBriefInteractive
19 | } from '../utils/brief-selection.js';
20 | import * as ui from '../utils/ui.js';
21 |
22 | /**
23 | * Result type from briefs command
24 | */
25 | export interface BriefsResult {
26 | success: boolean;
27 | action: 'list' | 'select' | 'create';
28 | briefs?: TagInfo[];
29 | currentBrief?: string | null;
30 | message?: string;
31 | }
32 |
33 | /**
34 | * BriefsCommand - Manage briefs for API storage (friendly alias)
35 | * Only works when using API storage (tryhamster.com)
36 | */
37 | export class BriefsCommand extends Command {
38 | private tmCore?: TmCore;
39 | private authManager: AuthManager;
40 | private lastResult?: BriefsResult;
41 |
42 | constructor(name?: string) {
43 | super(name || 'briefs');
44 |
45 | // Initialize auth manager
46 | this.authManager = AuthManager.getInstance();
47 |
48 | // Configure the command
49 | this.description('Manage briefs (API storage only)');
50 | this.alias('brief');
51 |
52 | // Add subcommands
53 | this.addListCommand();
54 | this.addSelectCommand();
55 | this.addCreateCommand();
56 |
57 | // Accept optional positional argument for brief URL/ID
58 | this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL');
59 |
60 | // Default action: if argument provided, select brief; else list briefs
61 | this.action(async (briefOrUrl?: string) => {
62 | if (briefOrUrl && briefOrUrl.trim().length > 0) {
63 | await this.executeSelectFromUrl(briefOrUrl.trim());
64 | return;
65 | }
66 | await this.executeList();
67 | });
68 | }
69 |
70 | /**
71 | * Check if user is authenticated (required for briefs)
72 | */
73 | private async checkAuth(): Promise<boolean> {
74 | return checkAuthentication(this.authManager, {
75 | message:
76 | 'The "briefs" command requires you to be logged in to your Hamster account.',
77 | footer:
78 | 'Working locally instead?\n' +
79 | ' → Use "task-master tags" for local tag management.',
80 | authCommand: 'task-master auth login'
81 | });
82 | }
83 |
84 | /**
85 | * Add list subcommand
86 | */
87 | private addListCommand(): void {
88 | this.command('list')
89 | .description('List all briefs (default action)')
90 | .option('--show-metadata', 'Show additional brief metadata')
91 | .addHelpText(
92 | 'after',
93 | `
94 | Examples:
95 | $ tm briefs # List all briefs (default)
96 | $ tm briefs list # List all briefs (explicit)
97 | $ tm briefs list --show-metadata # List with metadata
98 |
99 | Note: This command only works with API storage (tryhamster.com).
100 | `
101 | )
102 | .action(async (options) => {
103 | await this.executeList(options);
104 | });
105 | }
106 |
107 | /**
108 | * Add select subcommand
109 | */
110 | private addSelectCommand(): void {
111 | this.command('select')
112 | .description('Select a brief to work with')
113 | .argument(
114 | '[briefOrUrl]',
115 | 'Brief ID or Hamster URL (optional, interactive if omitted)'
116 | )
117 | .addHelpText(
118 | 'after',
119 | `
120 | Examples:
121 | $ tm brief select # Interactive selection
122 | $ tm brief select abc12345 # Select by ID
123 | $ tm brief select https://app.tryhamster.com/... # Select by URL
124 |
125 | Shortcuts:
126 | $ tm brief <brief-url> # Same as "select"
127 | $ tm brief # List all briefs
128 |
129 | Note: Works exactly like "tm context brief" - reuses the same interactive interface.
130 | `
131 | )
132 | .action(async (briefOrUrl) => {
133 | await this.executeSelect(briefOrUrl);
134 | });
135 | }
136 |
137 | /**
138 | * Add create subcommand
139 | */
140 | private addCreateCommand(): void {
141 | this.command('create')
142 | .description('Create a new brief (redirects to web UI)')
143 | .argument('[name]', 'Brief name (optional)')
144 | .addHelpText(
145 | 'after',
146 | `
147 | Examples:
148 | $ tm briefs create # Redirect to web UI to create brief
149 | $ tm briefs create my-new-brief # Redirect with suggested name
150 |
151 | Note: Briefs must be created through the Hamster Studio web interface.
152 | `
153 | )
154 | .action(async (name) => {
155 | await this.executeCreate(name);
156 | });
157 | }
158 |
159 | /**
160 | * Initialize TmCore if not already initialized
161 | */
162 | private async initTmCore(): Promise<void> {
163 | if (!this.tmCore) {
164 | this.tmCore = await createTmCore({
165 | projectPath: process.cwd()
166 | });
167 | }
168 | }
169 |
170 | /**
171 | * Execute list briefs
172 | */
173 | private async executeList(options?: {
174 | showMetadata?: boolean;
175 | }): Promise<void> {
176 | try {
177 | // Check authentication
178 | if (!(await this.checkAuth())) {
179 | process.exit(1);
180 | }
181 |
182 | // Use the bridge to list briefs
183 | const remoteResult = await tryListTagsViaRemote({
184 | projectRoot: process.cwd(),
185 | showMetadata: options?.showMetadata || false,
186 | report: (level: LogLevel, ...args: unknown[]) => {
187 | const message = args[0] as string;
188 | if (level === 'error') ui.displayError(message);
189 | else if (level === 'warn') ui.displayWarning(message);
190 | else if (level === 'info') ui.displayInfo(message);
191 | }
192 | });
193 |
194 | if (!remoteResult) {
195 | throw new Error('Failed to fetch briefs from API');
196 | }
197 |
198 | this.setLastResult({
199 | success: remoteResult.success,
200 | action: 'list',
201 | briefs: remoteResult.tags,
202 | currentBrief: remoteResult.currentTag,
203 | message: remoteResult.message
204 | });
205 | } catch (error) {
206 | ui.displayError(`Failed to list briefs: ${(error as Error).message}`);
207 | this.setLastResult({
208 | success: false,
209 | action: 'list',
210 | message: (error as Error).message
211 | });
212 | process.exit(1);
213 | }
214 | }
215 |
216 | /**
217 | * Execute select brief interactively or by name/ID
218 | */
219 | private async executeSelect(nameOrId?: string): Promise<void> {
220 | try {
221 | // Check authentication
222 | const hasSession = await this.authManager.hasValidSession();
223 | if (!hasSession) {
224 | ui.displayError('Not authenticated. Run "tm auth login" first.');
225 | process.exit(1);
226 | }
227 |
228 | // If name/ID provided, treat it as URL/ID selection
229 | if (nameOrId && nameOrId.trim().length > 0) {
230 | await this.executeSelectFromUrl(nameOrId.trim());
231 | return;
232 | }
233 |
234 | // Check if org is selected for interactive selection
235 | const context = this.authManager.getContext();
236 | if (!context?.orgId) {
237 | ui.displayErrorBox(
238 | 'No organization selected. Run "tm context org" first.'
239 | );
240 | process.exit(1);
241 | }
242 |
243 | // Use shared utility for interactive selection
244 | const result = await selectBriefInteractive(
245 | this.authManager,
246 | context.orgId
247 | );
248 |
249 | this.setLastResult({
250 | success: result.success,
251 | action: 'select',
252 | currentBrief: result.briefId,
253 | message: result.message
254 | });
255 |
256 | if (!result.success) {
257 | process.exit(1);
258 | }
259 | } catch (error) {
260 | ui.displayErrorBox(`Failed to select brief: ${(error as Error).message}`);
261 | this.setLastResult({
262 | success: false,
263 | action: 'select',
264 | message: (error as Error).message
265 | });
266 | process.exit(1);
267 | }
268 | }
269 |
270 | /**
271 | * Execute select brief from any input (URL, ID, or name)
272 | * All parsing logic is in tm-core
273 | */
274 | private async executeSelectFromUrl(input: string): Promise<void> {
275 | try {
276 | // Check authentication
277 | const hasSession = await this.authManager.hasValidSession();
278 | if (!hasSession) {
279 | ui.displayError('Not authenticated. Run "tm auth login" first.');
280 | process.exit(1);
281 | }
282 |
283 | // Initialize tmCore to access business logic
284 | await this.initTmCore();
285 |
286 | // Use shared utility - tm-core handles ALL parsing
287 | const result = await selectBriefFromInput(
288 | this.authManager,
289 | input,
290 | this.tmCore
291 | );
292 |
293 | this.setLastResult({
294 | success: result.success,
295 | action: 'select',
296 | currentBrief: result.briefId,
297 | message: result.message
298 | });
299 |
300 | if (!result.success) {
301 | process.exit(1);
302 | }
303 | } catch (error) {
304 | ui.displayErrorBox(`Failed to select brief: ${(error as Error).message}`);
305 | this.setLastResult({
306 | success: false,
307 | action: 'select',
308 | message: (error as Error).message
309 | });
310 | process.exit(1);
311 | }
312 | }
313 |
314 | /**
315 | * Execute create brief (redirect to web UI)
316 | */
317 | private async executeCreate(name?: string): Promise<void> {
318 | try {
319 | // Check authentication
320 | if (!(await this.checkAuth())) {
321 | process.exit(1);
322 | }
323 |
324 | // Use the bridge to redirect to web UI
325 | const remoteResult = await tryAddTagViaRemote({
326 | tagName: name || 'new-brief',
327 | projectRoot: process.cwd(),
328 | report: (level: LogLevel, ...args: unknown[]) => {
329 | const message = args[0] as string;
330 | if (level === 'error') ui.displayError(message);
331 | else if (level === 'warn') ui.displayWarning(message);
332 | else if (level === 'info') ui.displayInfo(message);
333 | }
334 | });
335 |
336 | if (!remoteResult) {
337 | throw new Error('Failed to get brief creation URL');
338 | }
339 |
340 | this.setLastResult({
341 | success: remoteResult.success,
342 | action: 'create',
343 | message: remoteResult.message
344 | });
345 |
346 | if (!remoteResult.success) {
347 | process.exit(1);
348 | }
349 | } catch (error) {
350 | ui.displayErrorBox(`Failed to create brief: ${(error as Error).message}`);
351 | this.setLastResult({
352 | success: false,
353 | action: 'create',
354 | message: (error as Error).message
355 | });
356 | process.exit(1);
357 | }
358 | }
359 |
360 | /**
361 | * Set the last result for programmatic access
362 | */
363 | private setLastResult(result: BriefsResult): void {
364 | this.lastResult = result;
365 | }
366 |
367 | /**
368 | * Get the last result (for programmatic usage)
369 | */
370 | getLastResult(): BriefsResult | undefined {
371 | return this.lastResult;
372 | }
373 |
374 | /**
375 | * Register this command on an existing program
376 | */
377 | static register(program: Command, name?: string): BriefsCommand {
378 | const briefsCommand = new BriefsCommand(name);
379 | program.addCommand(briefsCommand);
380 | return briefsCommand;
381 | }
382 | }
383 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/tasks/services/preflight-checker.service.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Preflight Checker Service
3 | * Validates environment and prerequisites for autopilot execution
4 | */
5 |
6 | import { readFileSync, existsSync, readdirSync } from 'node:fs';
7 | import { join } from 'path';
8 | import { execSync } from 'child_process';
9 | import { getLogger } from '../../../common/logger/factory.js';
10 | import {
11 | isGitRepository,
12 | isGhCliAvailable,
13 | getDefaultBranch
14 | } from '../../../common/utils/git-utils.js';
15 |
16 | const logger = getLogger('PreflightChecker');
17 |
18 | /**
19 | * Result of a single preflight check
20 | */
21 | export interface CheckResult {
22 | /** Whether the check passed */
23 | success: boolean;
24 | /** The value detected/validated */
25 | value?: any;
26 | /** Error or warning message */
27 | message?: string;
28 | }
29 |
30 | /**
31 | * Complete preflight validation results
32 | */
33 | export interface PreflightResult {
34 | /** Overall success - all checks passed */
35 | success: boolean;
36 | /** Test command detection result */
37 | testCommand: CheckResult;
38 | /** Git working tree status */
39 | gitWorkingTree: CheckResult;
40 | /** Required tools availability */
41 | requiredTools: CheckResult;
42 | /** Default branch detection */
43 | defaultBranch: CheckResult;
44 | /** Summary message */
45 | summary: string;
46 | }
47 |
48 | /**
49 | * Tool validation result
50 | */
51 | interface ToolCheck {
52 | name: string;
53 | available: boolean;
54 | version?: string;
55 | message?: string;
56 | }
57 |
58 | /**
59 | * PreflightChecker validates environment for autopilot execution
60 | */
61 | export class PreflightChecker {
62 | private projectRoot: string;
63 |
64 | constructor(projectRoot: string) {
65 | if (!projectRoot) {
66 | throw new Error('projectRoot is required for PreflightChecker');
67 | }
68 | this.projectRoot = projectRoot;
69 | }
70 |
71 | /**
72 | * Detect test command from package.json
73 | */
74 | async detectTestCommand(): Promise<CheckResult> {
75 | try {
76 | const packageJsonPath = join(this.projectRoot, 'package.json');
77 | const packageJsonContent = readFileSync(packageJsonPath, 'utf-8');
78 | const packageJson = JSON.parse(packageJsonContent);
79 |
80 | if (!packageJson.scripts || !packageJson.scripts.test) {
81 | return {
82 | success: false,
83 | message:
84 | 'No test script found in package.json. Please add a "test" script.'
85 | };
86 | }
87 |
88 | const testCommand = packageJson.scripts.test;
89 |
90 | return {
91 | success: true,
92 | value: testCommand,
93 | message: `Test command: ${testCommand}`
94 | };
95 | } catch (error: any) {
96 | if (error.code === 'ENOENT') {
97 | return {
98 | success: false,
99 | message: 'package.json not found in project root'
100 | };
101 | }
102 |
103 | return {
104 | success: false,
105 | message: `Failed to read package.json: ${error.message}`
106 | };
107 | }
108 | }
109 |
110 | /**
111 | * Check git working tree status
112 | */
113 | async checkGitWorkingTree(): Promise<CheckResult> {
114 | try {
115 | // Check if it's a git repository
116 | const isRepo = await isGitRepository(this.projectRoot);
117 | if (!isRepo) {
118 | return {
119 | success: false,
120 | message: 'Not a git repository. Initialize git first.'
121 | };
122 | }
123 |
124 | // Check for changes (staged/unstaged/untracked) without requiring HEAD
125 | const status = execSync('git status --porcelain', {
126 | cwd: this.projectRoot,
127 | encoding: 'utf-8',
128 | timeout: 5000
129 | });
130 | if (status.trim().length > 0) {
131 | return {
132 | success: false,
133 | value: 'dirty',
134 | message:
135 | 'Working tree has uncommitted or untracked changes. Please commit or stash them.'
136 | };
137 | }
138 | return {
139 | success: true,
140 | value: 'clean',
141 | message: 'Working tree is clean'
142 | };
143 | } catch (error: any) {
144 | return {
145 | success: false,
146 | message: `Git check failed: ${error.message}`
147 | };
148 | }
149 | }
150 |
151 | /**
152 | * Detect project types based on common configuration files
153 | */
154 | private detectProjectTypes(): string[] {
155 | const types: string[] = [];
156 |
157 | if (existsSync(join(this.projectRoot, 'package.json'))) types.push('node');
158 | if (
159 | existsSync(join(this.projectRoot, 'requirements.txt')) ||
160 | existsSync(join(this.projectRoot, 'setup.py')) ||
161 | existsSync(join(this.projectRoot, 'pyproject.toml'))
162 | )
163 | types.push('python');
164 | if (
165 | existsSync(join(this.projectRoot, 'pom.xml')) ||
166 | existsSync(join(this.projectRoot, 'build.gradle'))
167 | )
168 | types.push('java');
169 | if (existsSync(join(this.projectRoot, 'go.mod'))) types.push('go');
170 | if (existsSync(join(this.projectRoot, 'Cargo.toml'))) types.push('rust');
171 | if (existsSync(join(this.projectRoot, 'composer.json'))) types.push('php');
172 | if (existsSync(join(this.projectRoot, 'Gemfile'))) types.push('ruby');
173 | const files = readdirSync(this.projectRoot);
174 | if (files.some((f) => f.endsWith('.csproj') || f.endsWith('.sln')))
175 | types.push('dotnet');
176 |
177 | return types;
178 | }
179 |
180 | /**
181 | * Get required tools for a project type
182 | */
183 | private getToolsForProjectType(
184 | type: string
185 | ): Array<{ command: string; args: string[] }> {
186 | const toolMap: Record<
187 | string,
188 | Array<{ command: string; args: string[] }>
189 | > = {
190 | node: [
191 | { command: 'node', args: ['--version'] },
192 | { command: 'npm', args: ['--version'] }
193 | ],
194 | python: [
195 | { command: 'python3', args: ['--version'] },
196 | { command: 'pip3', args: ['--version'] }
197 | ],
198 | java: [{ command: 'java', args: ['--version'] }],
199 | go: [{ command: 'go', args: ['version'] }],
200 | rust: [{ command: 'cargo', args: ['--version'] }],
201 | php: [
202 | { command: 'php', args: ['--version'] },
203 | { command: 'composer', args: ['--version'] }
204 | ],
205 | ruby: [
206 | { command: 'ruby', args: ['--version'] },
207 | { command: 'bundle', args: ['--version'] }
208 | ],
209 | dotnet: [{ command: 'dotnet', args: ['--version'] }]
210 | };
211 |
212 | return toolMap[type] || [];
213 | }
214 |
215 | /**
216 | * Validate required tools availability
217 | */
218 | async validateRequiredTools(): Promise<CheckResult> {
219 | const tools: ToolCheck[] = [];
220 |
221 | // Always check git and gh CLI
222 | tools.push(this.checkTool('git', ['--version']));
223 | tools.push(await this.checkGhCli());
224 |
225 | // Detect project types and check their tools
226 | const projectTypes = this.detectProjectTypes();
227 |
228 | if (projectTypes.length === 0) {
229 | logger.warn('No recognized project type detected');
230 | } else {
231 | logger.info(`Detected project types: ${projectTypes.join(', ')}`);
232 | }
233 |
234 | for (const type of projectTypes) {
235 | const typeTools = this.getToolsForProjectType(type);
236 | for (const tool of typeTools) {
237 | tools.push(this.checkTool(tool.command, tool.args));
238 | }
239 | }
240 |
241 | // Determine overall success
242 | const allAvailable = tools.every((tool) => tool.available);
243 | const missingTools = tools
244 | .filter((tool) => !tool.available)
245 | .map((tool) => tool.name);
246 |
247 | if (!allAvailable) {
248 | return {
249 | success: false,
250 | value: tools,
251 | message: `Missing required tools: ${missingTools.join(', ')}`
252 | };
253 | }
254 |
255 | return {
256 | success: true,
257 | value: tools,
258 | message: 'All required tools are available'
259 | };
260 | }
261 |
262 | /**
263 | * Check if a command-line tool is available
264 | */
265 | private checkTool(command: string, versionArgs: string[]): ToolCheck {
266 | try {
267 | const version = execSync(`${command} ${versionArgs.join(' ')}`, {
268 | cwd: this.projectRoot,
269 | encoding: 'utf-8',
270 | stdio: 'pipe',
271 | timeout: 5000
272 | })
273 | .trim()
274 | .split('\n')[0];
275 |
276 | return {
277 | name: command,
278 | available: true,
279 | version,
280 | message: `${command} ${version}`
281 | };
282 | } catch (error) {
283 | return {
284 | name: command,
285 | available: false,
286 | message: `${command} not found`
287 | };
288 | }
289 | }
290 |
291 | /**
292 | * Check GitHub CLI installation and authentication status
293 | */
294 | private async checkGhCli(): Promise<ToolCheck> {
295 | try {
296 | const version = execSync('gh --version', {
297 | cwd: this.projectRoot,
298 | encoding: 'utf-8',
299 | stdio: 'pipe',
300 | timeout: 5000
301 | })
302 | .trim()
303 | .split('\n')[0];
304 | const authed = await isGhCliAvailable(this.projectRoot);
305 | return {
306 | name: 'gh',
307 | available: true,
308 | version,
309 | message: authed
310 | ? 'GitHub CLI installed (authenticated)'
311 | : 'GitHub CLI installed (not authenticated)'
312 | };
313 | } catch {
314 | return { name: 'gh', available: false, message: 'GitHub CLI not found' };
315 | }
316 | }
317 |
318 | /**
319 | * Detect default branch
320 | */
321 | async detectDefaultBranch(): Promise<CheckResult> {
322 | try {
323 | const defaultBranch = await getDefaultBranch(this.projectRoot);
324 |
325 | if (!defaultBranch) {
326 | return {
327 | success: false,
328 | message:
329 | 'Could not determine default branch. Make sure remote is configured.'
330 | };
331 | }
332 |
333 | return {
334 | success: true,
335 | value: defaultBranch,
336 | message: `Default branch: ${defaultBranch}`
337 | };
338 | } catch (error: any) {
339 | return {
340 | success: false,
341 | message: `Failed to detect default branch: ${error.message}`
342 | };
343 | }
344 | }
345 |
346 | /**
347 | * Run all preflight checks
348 | */
349 | async runAllChecks(): Promise<PreflightResult> {
350 | logger.info('Running preflight checks...');
351 |
352 | const testCommand = await this.detectTestCommand();
353 | const gitWorkingTree = await this.checkGitWorkingTree();
354 | const requiredTools = await this.validateRequiredTools();
355 | const defaultBranch = await this.detectDefaultBranch();
356 |
357 | const allSuccess =
358 | testCommand.success &&
359 | gitWorkingTree.success &&
360 | requiredTools.success &&
361 | defaultBranch.success;
362 |
363 | // Build summary
364 | const passed: string[] = [];
365 | const failed: string[] = [];
366 |
367 | if (testCommand.success) passed.push('Test command');
368 | else failed.push('Test command');
369 |
370 | if (gitWorkingTree.success) passed.push('Git working tree');
371 | else failed.push('Git working tree');
372 |
373 | if (requiredTools.success) passed.push('Required tools');
374 | else failed.push('Required tools');
375 |
376 | if (defaultBranch.success) passed.push('Default branch');
377 | else failed.push('Default branch');
378 |
379 | const total = passed.length + failed.length;
380 | const summary = allSuccess
381 | ? `All preflight checks passed (${passed.length}/${total})`
382 | : `Preflight checks failed: ${failed.join(', ')} (${passed.length}/${total} passed)`;
383 |
384 | logger.info(summary);
385 |
386 | return {
387 | success: allSuccess,
388 | testCommand,
389 | gitWorkingTree,
390 | requiredTools,
391 | defaultBranch,
392 | summary
393 | };
394 | }
395 | }
396 |
```
--------------------------------------------------------------------------------
/docs/examples/codex-cli-usage.md:
--------------------------------------------------------------------------------
```markdown
1 | # Codex CLI Provider Usage Examples
2 |
3 | This guide provides practical examples of using Task Master with the Codex CLI provider.
4 |
5 | ## Prerequisites
6 |
7 | Before using these examples, ensure you have:
8 |
9 | ```bash
10 | # 1. Codex CLI installed
11 | npm install -g @openai/codex
12 |
13 | # 2. Authenticated with ChatGPT
14 | codex login
15 |
16 | # 3. Codex CLI configured as your provider
17 | task-master models --set-main gpt-5-codex --codex-cli
18 | ```
19 |
20 | ## Example 1: Basic Task Creation
21 |
22 | Use Codex CLI to create tasks from a simple description:
23 |
24 | ```bash
25 | # Add a task with AI-powered enhancement
26 | task-master add-task --prompt="Implement user authentication with JWT" --research
27 | ```
28 |
29 | **What happens**:
30 | 1. Task Master sends your prompt to GPT-5-Codex via the CLI
31 | 2. The AI analyzes your request and generates a detailed task
32 | 3. The task is added to your `.taskmaster/tasks/tasks.json`
33 | 4. OAuth credentials are automatically used (no API key needed)
34 |
35 | ## Example 2: Parsing a Product Requirements Document
36 |
37 | Create a comprehensive task list from a PRD:
38 |
39 | ```bash
40 | # Create your PRD
41 | cat > my-feature.txt <<EOF
42 | # User Profile Feature
43 |
44 | ## Requirements
45 | 1. Users can view their profile
46 | 2. Users can edit their information
47 | 3. Profile pictures can be uploaded
48 | 4. Email verification required
49 |
50 | ## Technical Constraints
51 | - Use React for frontend
52 | - Node.js/Express backend
53 | - PostgreSQL database
54 | EOF
55 |
56 | # Parse with Codex CLI
57 | task-master parse-prd my-feature.txt --num-tasks 12
58 | ```
59 |
60 | **What happens**:
61 | 1. GPT-5-Codex reads and analyzes your PRD
62 | 2. Generates structured tasks with dependencies
63 | 3. Creates subtasks for complex items
64 | 4. Saves everything to `.taskmaster/tasks/`
65 |
66 | ## Example 3: Expanding Tasks with Research
67 |
68 | Break down a complex task into detailed subtasks:
69 |
70 | ```bash
71 | # First, show your current tasks
72 | task-master list
73 |
74 | # Expand a specific task (e.g., task 1.2)
75 | task-master expand --id=1.2 --research --force
76 | ```
77 |
78 | **What happens**:
79 | 1. Codex CLI uses GPT-5 for research-level analysis
80 | 2. Breaks down the task into logical subtasks
81 | 3. Adds implementation details and test strategies
82 | 4. Updates the task with dependency information
83 |
84 | ## Example 4: Analyzing Project Complexity
85 |
86 | Get AI-powered insights into your project's task complexity:
87 |
88 | ```bash
89 | # Analyze all tasks
90 | task-master analyze-complexity --research
91 |
92 | # View the complexity report
93 | task-master complexity-report
94 | ```
95 |
96 | **What happens**:
97 | 1. GPT-5 analyzes each task's scope and requirements
98 | 2. Assigns complexity scores and estimates subtask counts
99 | 3. Generates a detailed report
100 | 4. Saves to `.taskmaster/reports/task-complexity-report.json`
101 |
102 | ## Example 5: Using Custom Codex CLI Settings
103 |
104 | Configure Codex CLI behavior for different commands:
105 |
106 | ```json
107 | // In .taskmaster/config.json
108 | {
109 | "models": {
110 | "main": {
111 | "provider": "codex-cli",
112 | "modelId": "gpt-5-codex",
113 | "maxTokens": 128000,
114 | "temperature": 0.2
115 | }
116 | },
117 | "codexCli": {
118 | "allowNpx": true,
119 | "approvalMode": "on-failure",
120 | "sandboxMode": "workspace-write",
121 | "commandSpecific": {
122 | "parse-prd": {
123 | "verbose": true,
124 | "approvalMode": "never"
125 | },
126 | "expand": {
127 | "sandboxMode": "read-only",
128 | "verbose": true
129 | }
130 | }
131 | }
132 | }
133 | ```
134 |
135 | ```bash
136 | # Now parse-prd runs with verbose output and no approvals
137 | task-master parse-prd requirements.txt
138 |
139 | # Expand runs with read-only mode
140 | task-master expand --id=2.1
141 | ```
142 |
143 | ## Example 6: Workflow - Building a Feature End-to-End
144 |
145 | Complete workflow from PRD to implementation tracking:
146 |
147 | ```bash
148 | # Step 1: Initialize project
149 | task-master init
150 |
151 | # Step 2: Set up Codex CLI
152 | task-master models --set-main gpt-5-codex --codex-cli
153 | task-master models --set-fallback gpt-5 --codex-cli
154 |
155 | # Step 3: Create PRD
156 | cat > feature-prd.txt <<EOF
157 | # Authentication System
158 |
159 | Implement a complete authentication system with:
160 | - User registration
161 | - Email verification
162 | - Password reset
163 | - Two-factor authentication
164 | - Session management
165 | EOF
166 |
167 | # Step 4: Parse PRD into tasks
168 | task-master parse-prd feature-prd.txt --num-tasks 8
169 |
170 | # Step 5: Analyze complexity
171 | task-master analyze-complexity --research
172 |
173 | # Step 6: Expand complex tasks
174 | task-master expand --all --research
175 |
176 | # Step 7: Start working
177 | task-master next
178 | # Shows: Task 1.1: User registration database schema
179 |
180 | # Step 8: Mark completed as you work
181 | task-master set-status --id=1.1 --status=done
182 |
183 | # Step 9: Continue to next task
184 | task-master next
185 | ```
186 |
187 | ## Example 7: Multi-Role Configuration
188 |
189 | Use Codex CLI for main tasks, Perplexity for research:
190 |
191 | ```json
192 | // In .taskmaster/config.json
193 | {
194 | "models": {
195 | "main": {
196 | "provider": "codex-cli",
197 | "modelId": "gpt-5-codex",
198 | "maxTokens": 128000,
199 | "temperature": 0.2
200 | },
201 | "research": {
202 | "provider": "perplexity",
203 | "modelId": "sonar-pro",
204 | "maxTokens": 8700,
205 | "temperature": 0.1
206 | },
207 | "fallback": {
208 | "provider": "codex-cli",
209 | "modelId": "gpt-5",
210 | "maxTokens": 128000,
211 | "temperature": 0.2
212 | }
213 | }
214 | }
215 | ```
216 |
217 | ```bash
218 | # Main task operations use GPT-5-Codex
219 | task-master add-task --prompt="Build REST API endpoint"
220 |
221 | # Research operations use Perplexity
222 | task-master analyze-complexity --research
223 |
224 | # Fallback to GPT-5 if needed
225 | task-master expand --id=3.2 --force
226 | ```
227 |
228 | ## Example 8: Troubleshooting Common Issues
229 |
230 | ### Issue: Codex CLI not found
231 |
232 | ```bash
233 | # Check if Codex is installed
234 | codex --version
235 |
236 | # If not found, install globally
237 | npm install -g @openai/codex
238 |
239 | # Or enable npx fallback in config
240 | cat >> .taskmaster/config.json <<EOF
241 | {
242 | "codexCli": {
243 | "allowNpx": true
244 | }
245 | }
246 | EOF
247 | ```
248 |
249 | ### Issue: Not authenticated
250 |
251 | ```bash
252 | # Check auth status
253 | codex
254 | # Use /about command to see auth info
255 |
256 | # Re-authenticate if needed
257 | codex login
258 | ```
259 |
260 | ### Issue: Want more verbose output
261 |
262 | ```bash
263 | # Enable verbose mode in config
264 | cat >> .taskmaster/config.json <<EOF
265 | {
266 | "codexCli": {
267 | "verbose": true
268 | }
269 | }
270 | EOF
271 |
272 | # Or for specific commands
273 | task-master parse-prd my-prd.txt
274 | # (verbose output shows detailed Codex CLI interactions)
275 | ```
276 |
277 | ## Example 9: CI/CD Integration
278 |
279 | Use Codex CLI in automated workflows:
280 |
281 | ```yaml
282 | # .github/workflows/task-analysis.yml
283 | name: Analyze Task Complexity
284 |
285 | on:
286 | push:
287 | paths:
288 | - '.taskmaster/**'
289 |
290 | jobs:
291 | analyze:
292 | runs-on: ubuntu-latest
293 | steps:
294 | - uses: actions/checkout@v4
295 |
296 | - name: Setup Node.js
297 | uses: actions/setup-node@v4
298 | with:
299 | node-version: '20'
300 |
301 | - name: Install Task Master
302 | run: npm install -g task-master-ai
303 |
304 | - name: Configure Codex CLI
305 | run: |
306 | npm install -g @openai/codex
307 | echo "${{ secrets.OPENAI_CODEX_API_KEY }}" > ~/.codex-auth
308 | env:
309 | OPENAI_CODEX_API_KEY: ${{ secrets.OPENAI_CODEX_API_KEY }}
310 |
311 | - name: Configure Task Master
312 | run: |
313 | cat > .taskmaster/config.json <<EOF
314 | {
315 | "models": {
316 | "main": {
317 | "provider": "codex-cli",
318 | "modelId": "gpt-5"
319 | }
320 | },
321 | "codexCli": {
322 | "allowNpx": true,
323 | "skipGitRepoCheck": true,
324 | "approvalMode": "never",
325 | "fullAuto": true
326 | }
327 | }
328 | EOF
329 |
330 | - name: Analyze Complexity
331 | run: task-master analyze-complexity --research
332 |
333 | - name: Upload Report
334 | uses: actions/upload-artifact@v3
335 | with:
336 | name: complexity-report
337 | path: .taskmaster/reports/task-complexity-report.json
338 | ```
339 |
340 | ## Best Practices
341 |
342 | ### 1. Use OAuth for Development
343 |
344 | ```bash
345 | # For local development, use OAuth (no API key needed)
346 | codex login
347 | task-master models --set-main gpt-5-codex --codex-cli
348 | ```
349 |
350 | ### 2. Configure Approval Modes Appropriately
351 |
352 | ```json
353 | {
354 | "codexCli": {
355 | "approvalMode": "on-failure", // Safe default
356 | "sandboxMode": "workspace-write" // Restricts to project directory
357 | }
358 | }
359 | ```
360 |
361 | ### 3. Use Command-Specific Settings
362 |
363 | ```json
364 | {
365 | "codexCli": {
366 | "commandSpecific": {
367 | "parse-prd": {
368 | "approvalMode": "never", // PRD parsing is safe
369 | "verbose": true
370 | },
371 | "expand": {
372 | "approvalMode": "on-request", // More cautious for task expansion
373 | "verbose": false
374 | }
375 | }
376 | }
377 | }
378 | ```
379 |
380 | ### 4. Leverage Codebase Analysis
381 |
382 | ```json
383 | {
384 | "global": {
385 | "enableCodebaseAnalysis": true // Let Codex analyze your code
386 | }
387 | }
388 | ```
389 |
390 | ### 5. Handle Errors Gracefully
391 |
392 | ```bash
393 | # Always configure a fallback model
394 | task-master models --set-fallback gpt-5 --codex-cli
395 |
396 | # Or use a different provider as fallback
397 | task-master models --set-fallback claude-3-5-sonnet
398 | ```
399 |
400 | ## Next Steps
401 |
402 | - Read the [Codex CLI Provider Documentation](../providers/codex-cli.md)
403 | - Explore [Configuration Options](../configuration.md#codex-cli-provider)
404 | - Check out [Command Reference](../command-reference.md)
405 | - Learn about [Task Structure](../task-structure.md)
406 |
407 | ## Common Patterns
408 |
409 | ### Pattern: Daily Development Workflow
410 |
411 | ```bash
412 | # Morning: Review tasks
413 | task-master list
414 |
415 | # Get next task
416 | task-master next
417 |
418 | # Work on task...
419 |
420 | # Update task with notes
421 | task-master update-subtask --id=2.3 --prompt="Implemented authentication middleware"
422 |
423 | # Mark complete
424 | task-master set-status --id=2.3 --status=done
425 |
426 | # Repeat
427 | ```
428 |
429 | ### Pattern: Feature Planning
430 |
431 | ```bash
432 | # Write feature spec
433 | vim new-feature.txt
434 |
435 | # Generate tasks
436 | task-master parse-prd new-feature.txt --num-tasks 10
437 |
438 | # Analyze and expand
439 | task-master analyze-complexity --research
440 | task-master expand --all --research --force
441 |
442 | # Review and adjust
443 | task-master list
444 | ```
445 |
446 | ### Pattern: Sprint Planning
447 |
448 | ```bash
449 | # Parse sprint requirements
450 | task-master parse-prd sprint-requirements.txt
451 |
452 | # Analyze complexity
453 | task-master analyze-complexity --research
454 |
455 | # View report
456 | task-master complexity-report
457 |
458 | # Adjust task estimates based on complexity scores
459 | ```
460 |
461 | ---
462 |
463 | For more examples and advanced usage, see the [full documentation](https://docs.task-master.dev).
464 |
```
--------------------------------------------------------------------------------
/apps/cli/src/utils/auto-update.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Auto-update utilities for task-master-ai CLI
3 | */
4 |
5 | import { spawn } from 'child_process';
6 | import https from 'https';
7 | import boxen from 'boxen';
8 | import chalk from 'chalk';
9 | import ora from 'ora';
10 | import process from 'process';
11 |
12 | export interface UpdateInfo {
13 | currentVersion: string;
14 | latestVersion: string;
15 | needsUpdate: boolean;
16 | highlights?: string[];
17 | }
18 |
19 | /**
20 | * Get current version from build-time injected environment variable
21 | */
22 | function getCurrentVersion(): string {
23 | // Version is injected at build time via TM_PUBLIC_VERSION
24 | const version = process.env.TM_PUBLIC_VERSION;
25 | if (version && version !== 'unknown') {
26 | return version;
27 | }
28 |
29 | // Fallback for development or if injection failed
30 | console.warn('Could not read version from TM_PUBLIC_VERSION, using fallback');
31 | return '0.0.0';
32 | }
33 |
34 | /**
35 | * Compare semantic versions with proper pre-release handling
36 | * @param v1 - First version
37 | * @param v2 - Second version
38 | * @returns -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
39 | */
40 | export function compareVersions(v1: string, v2: string): number {
41 | const toParts = (v: string) => {
42 | const [core, pre = ''] = v.split('-', 2);
43 | const nums = core.split('.').map((n) => Number.parseInt(n, 10) || 0);
44 | return { nums, pre };
45 | };
46 |
47 | const a = toParts(v1);
48 | const b = toParts(v2);
49 | const len = Math.max(a.nums.length, b.nums.length);
50 |
51 | // Compare numeric parts
52 | for (let i = 0; i < len; i++) {
53 | const d = (a.nums[i] || 0) - (b.nums[i] || 0);
54 | if (d !== 0) return d < 0 ? -1 : 1;
55 | }
56 |
57 | // Handle pre-release comparison
58 | if (a.pre && !b.pre) return -1; // prerelease < release
59 | if (!a.pre && b.pre) return 1; // release > prerelease
60 | if (a.pre === b.pre) return 0; // same or both empty
61 | return a.pre < b.pre ? -1 : 1; // basic prerelease tie-break
62 | }
63 |
64 | /**
65 | * Fetch CHANGELOG.md from GitHub and extract highlights for a specific version
66 | */
67 | async function fetchChangelogHighlights(version: string): Promise<string[]> {
68 | return new Promise((resolve) => {
69 | const options = {
70 | hostname: 'raw.githubusercontent.com',
71 | path: '/eyaltoledano/claude-task-master/main/CHANGELOG.md',
72 | method: 'GET',
73 | headers: {
74 | 'User-Agent': `task-master-ai/${version}`
75 | }
76 | };
77 |
78 | const req = https.request(options, (res) => {
79 | let data = '';
80 |
81 | res.on('data', (chunk) => {
82 | data += chunk;
83 | });
84 |
85 | res.on('end', () => {
86 | try {
87 | if (res.statusCode !== 200) {
88 | resolve([]);
89 | return;
90 | }
91 |
92 | const highlights = parseChangelogHighlights(data, version);
93 | resolve(highlights);
94 | } catch (error) {
95 | resolve([]);
96 | }
97 | });
98 | });
99 |
100 | req.on('error', () => {
101 | resolve([]);
102 | });
103 |
104 | req.setTimeout(3000, () => {
105 | req.destroy();
106 | resolve([]);
107 | });
108 |
109 | req.end();
110 | });
111 | }
112 |
113 | /**
114 | * Parse changelog markdown to extract Minor Changes for a specific version
115 | * @internal - Exported for testing purposes only
116 | */
117 | export function parseChangelogHighlights(
118 | changelog: string,
119 | version: string
120 | ): string[] {
121 | try {
122 | // Validate version format (basic semver pattern) to prevent ReDoS
123 | if (!/^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?$/.test(version)) {
124 | return [];
125 | }
126 |
127 | // Find the version section
128 | const versionRegex = new RegExp(
129 | `## ${version.replace(/\./g, '\\.')}\\s*\\n`,
130 | 'i'
131 | );
132 | const versionMatch = changelog.match(versionRegex);
133 |
134 | if (!versionMatch) {
135 | return [];
136 | }
137 |
138 | // Extract content from this version to the next version heading
139 | const startIdx = versionMatch.index! + versionMatch[0].length;
140 | const nextVersionIdx = changelog.indexOf('\n## ', startIdx);
141 | const versionContent =
142 | nextVersionIdx > 0
143 | ? changelog.slice(startIdx, nextVersionIdx)
144 | : changelog.slice(startIdx);
145 |
146 | // Find Minor Changes section
147 | const minorChangesMatch = versionContent.match(
148 | /### Minor Changes\s*\n([\s\S]*?)(?=\n###|\n##|$)/i
149 | );
150 |
151 | if (!minorChangesMatch) {
152 | return [];
153 | }
154 |
155 | const minorChangesContent = minorChangesMatch[1];
156 | const highlights: string[] = [];
157 |
158 | // Extract all bullet points (lines starting with -)
159 | // Format: - [#PR](...) Thanks [@author]! - Description
160 | const bulletRegex = /^-\s+\[#\d+\][^\n]*?!\s+-\s+(.+?)$/gm;
161 | let match;
162 |
163 | while ((match = bulletRegex.exec(minorChangesContent)) !== null) {
164 | const desc = match[1].trim();
165 | highlights.push(desc);
166 | }
167 |
168 | return highlights;
169 | } catch (error) {
170 | return [];
171 | }
172 | }
173 |
174 | /**
175 | * Check for newer version of task-master-ai
176 | */
177 | export async function checkForUpdate(
178 | currentVersionOverride?: string
179 | ): Promise<UpdateInfo> {
180 | const currentVersion = currentVersionOverride || getCurrentVersion();
181 |
182 | return new Promise((resolve) => {
183 | const options = {
184 | hostname: 'registry.npmjs.org',
185 | path: '/task-master-ai',
186 | method: 'GET',
187 | headers: {
188 | Accept: 'application/vnd.npm.install-v1+json',
189 | 'User-Agent': `task-master-ai/${currentVersion}`
190 | }
191 | };
192 |
193 | const req = https.request(options, (res) => {
194 | let data = '';
195 |
196 | res.on('data', (chunk) => {
197 | data += chunk;
198 | });
199 |
200 | res.on('end', async () => {
201 | try {
202 | if (res.statusCode !== 200)
203 | throw new Error(`npm registry status ${res.statusCode}`);
204 | const npmData = JSON.parse(data);
205 | const latestVersion = npmData['dist-tags']?.latest || currentVersion;
206 |
207 | const needsUpdate =
208 | compareVersions(currentVersion, latestVersion) < 0;
209 |
210 | // Fetch highlights if update is needed
211 | let highlights: string[] | undefined;
212 | if (needsUpdate) {
213 | highlights = await fetchChangelogHighlights(latestVersion);
214 | }
215 |
216 | resolve({
217 | currentVersion,
218 | latestVersion,
219 | needsUpdate,
220 | highlights
221 | });
222 | } catch (error) {
223 | resolve({
224 | currentVersion,
225 | latestVersion: currentVersion,
226 | needsUpdate: false
227 | });
228 | }
229 | });
230 | });
231 |
232 | req.on('error', () => {
233 | resolve({
234 | currentVersion,
235 | latestVersion: currentVersion,
236 | needsUpdate: false
237 | });
238 | });
239 |
240 | req.setTimeout(3000, () => {
241 | req.destroy();
242 | resolve({
243 | currentVersion,
244 | latestVersion: currentVersion,
245 | needsUpdate: false
246 | });
247 | });
248 |
249 | req.end();
250 | });
251 | }
252 |
253 | /**
254 | * Display upgrade notification message
255 | */
256 | export function displayUpgradeNotification(
257 | currentVersion: string,
258 | latestVersion: string,
259 | highlights?: string[]
260 | ) {
261 | let content = `${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}`;
262 |
263 | if (highlights && highlights.length > 0) {
264 | content += '\n\n' + chalk.bold("What's New:");
265 | for (const highlight of highlights) {
266 | content += '\n' + chalk.cyan('• ') + highlight;
267 | }
268 | content += '\n\n' + 'Auto-updating to the latest version...';
269 | } else {
270 | content +=
271 | '\n\n' +
272 | 'Auto-updating to the latest version with new features and bug fixes...';
273 | }
274 |
275 | const message = boxen(content, {
276 | padding: 1,
277 | margin: { top: 1, bottom: 1 },
278 | borderColor: 'yellow',
279 | borderStyle: 'round'
280 | });
281 |
282 | console.log(message);
283 | }
284 |
285 | /**
286 | * Automatically update task-master-ai to the latest version
287 | */
288 | export async function performAutoUpdate(
289 | latestVersion: string
290 | ): Promise<boolean> {
291 | if (
292 | process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1' ||
293 | process.env.CI ||
294 | process.env.NODE_ENV === 'test'
295 | ) {
296 | const reason =
297 | process.env.TASKMASTER_SKIP_AUTO_UPDATE === '1'
298 | ? 'TASKMASTER_SKIP_AUTO_UPDATE=1'
299 | : process.env.CI
300 | ? 'CI environment'
301 | : 'NODE_ENV=test';
302 | console.log(chalk.dim(`Skipping auto-update (${reason})`));
303 | return false;
304 | }
305 | const spinner = ora({
306 | text: chalk.blue(
307 | `Updating task-master-ai to version ${chalk.green(latestVersion)}`
308 | ),
309 | spinner: 'dots',
310 | color: 'blue'
311 | }).start();
312 |
313 | return new Promise((resolve) => {
314 | const updateProcess = spawn(
315 | 'npm',
316 | [
317 | 'install',
318 | '-g',
319 | `task-master-ai@${latestVersion}`,
320 | '--no-fund',
321 | '--no-audit',
322 | '--loglevel=warn'
323 | ],
324 | {
325 | stdio: ['ignore', 'pipe', 'pipe']
326 | }
327 | );
328 |
329 | let errorOutput = '';
330 |
331 | updateProcess.stdout.on('data', () => {
332 | // Update spinner text with progress
333 | spinner.text = chalk.blue(
334 | `Installing task-master-ai@${latestVersion}...`
335 | );
336 | });
337 |
338 | updateProcess.stderr.on('data', (data) => {
339 | errorOutput += data.toString();
340 | });
341 |
342 | updateProcess.on('close', (code) => {
343 | if (code === 0) {
344 | spinner.succeed(
345 | chalk.green(
346 | `Successfully updated to version ${chalk.bold(latestVersion)}`
347 | )
348 | );
349 | resolve(true);
350 | } else {
351 | spinner.fail(chalk.red('Auto-update failed'));
352 | console.log(
353 | chalk.cyan(
354 | `Please run manually: npm install -g task-master-ai@${latestVersion}`
355 | )
356 | );
357 | if (errorOutput) {
358 | console.log(chalk.dim(`Error: ${errorOutput.trim()}`));
359 | }
360 | resolve(false);
361 | }
362 | });
363 |
364 | updateProcess.on('error', (error) => {
365 | spinner.fail(chalk.red('Auto-update failed'));
366 | console.log(chalk.red('Error:'), error.message);
367 | console.log(
368 | chalk.cyan(
369 | `Please run manually: npm install -g task-master-ai@${latestVersion}`
370 | )
371 | );
372 | resolve(false);
373 | });
374 | });
375 | }
376 |
377 | /**
378 | * Restart the CLI with the newly installed version
379 | * @param argv - Original command-line arguments (process.argv)
380 | */
381 | export function restartWithNewVersion(argv: string[]): void {
382 | const args = argv.slice(2); // Remove 'node' and script path
383 |
384 | console.log(chalk.dim('Restarting with updated version...\n'));
385 |
386 | // Spawn the updated task-master command
387 | const child = spawn('task-master', args, {
388 | stdio: 'inherit', // Inherit stdin/stdout/stderr so it looks seamless
389 | detached: false,
390 | shell: process.platform === 'win32' // Windows compatibility
391 | });
392 |
393 | child.on('exit', (code, signal) => {
394 | if (signal) {
395 | process.kill(process.pid, signal);
396 | return;
397 | }
398 | process.exit(code ?? 0);
399 | });
400 |
401 | child.on('error', (error) => {
402 | console.error(
403 | chalk.red('Failed to restart with new version:'),
404 | error.message
405 | );
406 | console.log(chalk.yellow('Please run your command again manually.'));
407 | process.exit(1);
408 | });
409 | }
410 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/tasks/services/task-loader.service.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Task Loader Service
3 | * Loads and validates tasks for autopilot execution
4 | */
5 |
6 | import type { Task, Subtask } from '../../../common/types/index.js';
7 | import type { TaskService } from './task-service.js';
8 | import { getLogger } from '../../../common/logger/factory.js';
9 | import { isTaskComplete } from '../../../common/constants/index.js';
10 |
11 | const logger = getLogger('TaskLoader');
12 |
13 | /**
14 | * Validation error types
15 | */
16 | export type ValidationErrorType =
17 | | 'task_not_found'
18 | | 'task_completed'
19 | | 'no_subtasks'
20 | | 'circular_dependencies'
21 | | 'missing_dependencies'
22 | | 'invalid_structure';
23 |
24 | /**
25 | * Validation result for task loading
26 | */
27 | export interface TaskValidationResult {
28 | /** Whether validation passed */
29 | success: boolean;
30 | /** Loaded task (only present if validation succeeded) */
31 | task?: Task;
32 | /** Error type */
33 | errorType?: ValidationErrorType;
34 | /** Human-readable error message */
35 | errorMessage?: string;
36 | /** Actionable suggestion for fixing the error */
37 | suggestion?: string;
38 | /** Dependency analysis (only for dependency errors) */
39 | dependencyIssues?: DependencyIssue[];
40 | }
41 |
42 | /**
43 | * Dependency issue details
44 | */
45 | export interface DependencyIssue {
46 | /** Subtask ID with the issue */
47 | subtaskId: string;
48 | /** Type of dependency issue */
49 | issueType: 'circular' | 'missing' | 'invalid';
50 | /** Description of the issue */
51 | message: string;
52 | /** The problematic dependency reference */
53 | dependencyRef?: string;
54 | }
55 |
56 | /**
57 | * TaskLoaderService loads and validates tasks for autopilot execution
58 | */
59 | export class TaskLoaderService {
60 | private taskService: TaskService;
61 |
62 | constructor(taskService: TaskService) {
63 | if (!taskService) {
64 | throw new Error('taskService is required for TaskLoaderService');
65 | }
66 | this.taskService = taskService;
67 | }
68 |
69 | /**
70 | * Load and validate a task for autopilot execution
71 | */
72 | async loadAndValidateTask(taskId: string): Promise<TaskValidationResult> {
73 | logger.info(`Loading task ${taskId}...`);
74 |
75 | // Step 1: Load task
76 | const task = await this.loadTask(taskId);
77 | if (!task) {
78 | return {
79 | success: false,
80 | errorType: 'task_not_found',
81 | errorMessage: `Task with ID "${taskId}" not found`,
82 | suggestion:
83 | 'Use "task-master list" to see available tasks or verify the task ID is correct.'
84 | };
85 | }
86 |
87 | // Step 2: Validate task status
88 | const statusValidation = this.validateTaskStatus(task);
89 | if (!statusValidation.success) {
90 | return statusValidation;
91 | }
92 |
93 | // Step 3: Check for subtasks
94 | const subtaskValidation = this.validateSubtasksExist(task);
95 | if (!subtaskValidation.success) {
96 | return subtaskValidation;
97 | }
98 |
99 | // Step 4: Validate subtask structure
100 | const structureValidation = this.validateSubtaskStructure(task);
101 | if (!structureValidation.success) {
102 | return structureValidation;
103 | }
104 |
105 | // Step 5: Analyze dependencies
106 | const dependencyValidation = this.validateDependencies(task);
107 | if (!dependencyValidation.success) {
108 | return dependencyValidation;
109 | }
110 |
111 | logger.info(`Task ${taskId} validated successfully`);
112 |
113 | return {
114 | success: true,
115 | task
116 | };
117 | }
118 |
119 | /**
120 | * Load task using TaskService
121 | */
122 | private async loadTask(taskId: string): Promise<Task | null> {
123 | try {
124 | return await this.taskService.getTask(taskId);
125 | } catch (error) {
126 | logger.error(`Failed to load task ${taskId}:`, error);
127 | return null;
128 | }
129 | }
130 |
131 | /**
132 | * Validate task status is appropriate for autopilot
133 | */
134 | private validateTaskStatus(task: Task): TaskValidationResult {
135 | if (isTaskComplete(task.status)) {
136 | return {
137 | success: false,
138 | errorType: 'task_completed',
139 | errorMessage: `Task "${task.title}" is already ${task.status}`,
140 | suggestion:
141 | 'Autopilot can only execute tasks that are pending or in-progress. Use a different task.'
142 | };
143 | }
144 |
145 | return { success: true };
146 | }
147 |
148 | /**
149 | * Validate task has subtasks
150 | */
151 | private validateSubtasksExist(task: Task): TaskValidationResult {
152 | if (!task.subtasks || task.subtasks.length === 0) {
153 | return {
154 | success: false,
155 | errorType: 'no_subtasks',
156 | errorMessage: `Task "${task.title}" has no subtasks`,
157 | suggestion: this.buildExpansionSuggestion(task)
158 | };
159 | }
160 |
161 | return { success: true };
162 | }
163 |
164 | /**
165 | * Build helpful suggestion for expanding tasks
166 | */
167 | private buildExpansionSuggestion(task: Task): string {
168 | const suggestions: string[] = [
169 | `Autopilot requires tasks to be broken down into subtasks for execution.`
170 | ];
171 |
172 | // Add expansion command suggestion
173 | suggestions.push(`\nExpand this task using:`);
174 | suggestions.push(` task-master expand --id=${task.id}`);
175 |
176 | // If task has complexity analysis, mention it
177 | if (task.complexity || task.recommendedSubtasks) {
178 | suggestions.push(
179 | `\nThis task has complexity analysis available. Consider reviewing it first:`
180 | );
181 | suggestions.push(` task-master show ${task.id}`);
182 | } else {
183 | suggestions.push(
184 | `\nOr analyze task complexity first to determine optimal subtask count:`
185 | );
186 | suggestions.push(` task-master analyze-complexity --from=${task.id}`);
187 | }
188 |
189 | return suggestions.join('\n');
190 | }
191 |
192 | /**
193 | * Validate subtask structure
194 | */
195 | private validateSubtaskStructure(task: Task): TaskValidationResult {
196 | for (const subtask of task.subtasks) {
197 | // Check required fields
198 | if (!subtask.title || !subtask.description) {
199 | return {
200 | success: false,
201 | errorType: 'invalid_structure',
202 | errorMessage: `Subtask ${task.id}.${subtask.id} is missing required fields`,
203 | suggestion:
204 | 'Subtasks must have title and description. Re-expand the task or manually fix the subtask structure.'
205 | };
206 | }
207 |
208 | // Validate dependencies are arrays
209 | if (subtask.dependencies && !Array.isArray(subtask.dependencies)) {
210 | return {
211 | success: false,
212 | errorType: 'invalid_structure',
213 | errorMessage: `Subtask ${task.id}.${subtask.id} has invalid dependencies format`,
214 | suggestion:
215 | 'Dependencies must be an array. Fix the task structure manually.'
216 | };
217 | }
218 | }
219 |
220 | return { success: true };
221 | }
222 |
223 | /**
224 | * Validate subtask dependencies
225 | */
226 | private validateDependencies(task: Task): TaskValidationResult {
227 | const issues: DependencyIssue[] = [];
228 | const subtaskIds = new Set(task.subtasks.map((st) => String(st.id)));
229 |
230 | for (const subtask of task.subtasks) {
231 | const subtaskId = `${task.id}.${subtask.id}`;
232 |
233 | // Check for missing dependencies
234 | if (subtask.dependencies && subtask.dependencies.length > 0) {
235 | for (const depId of subtask.dependencies) {
236 | const depIdStr = String(depId);
237 |
238 | if (!subtaskIds.has(depIdStr)) {
239 | issues.push({
240 | subtaskId,
241 | issueType: 'missing',
242 | message: `References non-existent subtask ${depIdStr}`,
243 | dependencyRef: depIdStr
244 | });
245 | }
246 | }
247 | }
248 |
249 | // Check for circular dependencies
250 | const circularCheck = this.detectCircularDependency(
251 | subtask,
252 | task.subtasks,
253 | new Set()
254 | );
255 |
256 | if (circularCheck) {
257 | issues.push({
258 | subtaskId,
259 | issueType: 'circular',
260 | message: `Circular dependency detected: ${circularCheck.join(' -> ')}`
261 | });
262 | }
263 | }
264 |
265 | if (issues.length > 0) {
266 | const errorType =
267 | issues[0].issueType === 'circular'
268 | ? 'circular_dependencies'
269 | : 'missing_dependencies';
270 |
271 | return {
272 | success: false,
273 | errorType,
274 | errorMessage: `Task "${task.title}" has dependency issues`,
275 | suggestion:
276 | 'Fix dependency issues manually or re-expand the task:\n' +
277 | issues
278 | .map((issue) => ` - ${issue.subtaskId}: ${issue.message}`)
279 | .join('\n'),
280 | dependencyIssues: issues
281 | };
282 | }
283 |
284 | return { success: true };
285 | }
286 |
287 | /**
288 | * Detect circular dependencies using depth-first search
289 | */
290 | private detectCircularDependency(
291 | subtask: Subtask,
292 | allSubtasks: Subtask[],
293 | visited: Set<string>
294 | ): string[] | null {
295 | const subtaskId = String(subtask.id);
296 |
297 | if (visited.has(subtaskId)) {
298 | return [subtaskId];
299 | }
300 |
301 | visited.add(subtaskId);
302 |
303 | if (subtask.dependencies && subtask.dependencies.length > 0) {
304 | for (const depId of subtask.dependencies) {
305 | const depIdStr = String(depId);
306 | const dependency = allSubtasks.find((st) => String(st.id) === depIdStr);
307 |
308 | if (dependency) {
309 | const circular = this.detectCircularDependency(
310 | dependency,
311 | allSubtasks,
312 | new Set(visited)
313 | );
314 |
315 | if (circular) {
316 | return [subtaskId, ...circular];
317 | }
318 | }
319 | }
320 | }
321 |
322 | return null;
323 | }
324 |
325 | /**
326 | * Get ordered subtask execution sequence
327 | * Returns subtasks in dependency order (tasks with no deps first)
328 | */
329 | getExecutionOrder(task: Task): Subtask[] {
330 | const ordered: Subtask[] = [];
331 | const completed = new Set<string>();
332 |
333 | // Keep adding subtasks whose dependencies are all completed
334 | while (ordered.length < task.subtasks.length) {
335 | let added = false;
336 |
337 | for (const subtask of task.subtasks) {
338 | const subtaskId = String(subtask.id);
339 |
340 | if (completed.has(subtaskId)) {
341 | continue;
342 | }
343 |
344 | // Check if all dependencies are completed
345 | const allDepsCompleted =
346 | !subtask.dependencies ||
347 | subtask.dependencies.length === 0 ||
348 | subtask.dependencies.every((depId) => completed.has(String(depId)));
349 |
350 | if (allDepsCompleted) {
351 | ordered.push(subtask);
352 | completed.add(subtaskId);
353 | added = true;
354 | break;
355 | }
356 | }
357 |
358 | // Safety check to prevent infinite loop
359 | if (!added && ordered.length < task.subtasks.length) {
360 | logger.warn(
361 | `Could not determine complete execution order for task ${task.id}`
362 | );
363 | // Add remaining subtasks in original order
364 | for (const subtask of task.subtasks) {
365 | if (!completed.has(String(subtask.id))) {
366 | ordered.push(subtask);
367 | }
368 | }
369 | break;
370 | }
371 | }
372 |
373 | return ordered;
374 | }
375 |
376 | /**
377 | * Clean up resources
378 | */
379 | async cleanup(): Promise<void> {
380 | // TaskService doesn't require explicit cleanup
381 | // Resources are automatically released when instance is garbage collected
382 | }
383 | }
384 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/logger/logger.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Tests for MCP logging integration
3 | */
4 |
5 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6 | import { type LogCallback, LogLevel, Logger } from './logger.js';
7 |
8 | describe('Logger - MCP Integration', () => {
9 | // Store original environment
10 | let originalEnv: Record<string, string | undefined>;
11 |
12 | beforeEach(() => {
13 | // Save original environment
14 | originalEnv = {
15 | MCP_MODE: process.env.MCP_MODE,
16 | TASK_MASTER_MCP: process.env.TASK_MASTER_MCP,
17 | TASK_MASTER_SILENT: process.env.TASK_MASTER_SILENT,
18 | TM_SILENT: process.env.TM_SILENT,
19 | TASK_MASTER_LOG_LEVEL: process.env.TASK_MASTER_LOG_LEVEL,
20 | TM_LOG_LEVEL: process.env.TM_LOG_LEVEL,
21 | NO_COLOR: process.env.NO_COLOR,
22 | TASK_MASTER_NO_COLOR: process.env.TASK_MASTER_NO_COLOR
23 | };
24 |
25 | // Clear environment variables for clean tests
26 | delete process.env.MCP_MODE;
27 | delete process.env.TASK_MASTER_MCP;
28 | delete process.env.TASK_MASTER_SILENT;
29 | delete process.env.TM_SILENT;
30 | delete process.env.TASK_MASTER_LOG_LEVEL;
31 | delete process.env.TM_LOG_LEVEL;
32 | delete process.env.NO_COLOR;
33 | delete process.env.TASK_MASTER_NO_COLOR;
34 | });
35 |
36 | afterEach(() => {
37 | // Restore original environment
38 | for (const [key, value] of Object.entries(originalEnv)) {
39 | if (value === undefined) {
40 | delete process.env[key];
41 | } else {
42 | process.env[key] = value;
43 | }
44 | }
45 | });
46 | describe('Callback-based logging', () => {
47 | it('should call callback instead of console when logCallback is provided', () => {
48 | const mockCallback = vi.fn();
49 | const logger = new Logger({
50 | level: LogLevel.INFO,
51 | logCallback: mockCallback
52 | });
53 |
54 | logger.info('Test message');
55 |
56 | expect(mockCallback).toHaveBeenCalledWith(
57 | 'info',
58 | expect.stringContaining('Test message')
59 | );
60 | });
61 |
62 | it('should call callback for all log levels', () => {
63 | const mockCallback = vi.fn();
64 | const logger = new Logger({
65 | level: LogLevel.DEBUG,
66 | logCallback: mockCallback
67 | });
68 |
69 | logger.error('Error message');
70 | logger.warn('Warning message');
71 | logger.info('Info message');
72 | logger.debug('Debug message');
73 |
74 | expect(mockCallback).toHaveBeenNthCalledWith(
75 | 1,
76 | 'error',
77 | expect.stringContaining('Error message')
78 | );
79 | expect(mockCallback).toHaveBeenNthCalledWith(
80 | 2,
81 | 'warn',
82 | expect.stringContaining('Warning message')
83 | );
84 | expect(mockCallback).toHaveBeenNthCalledWith(
85 | 3,
86 | 'info',
87 | expect.stringContaining('Info message')
88 | );
89 | expect(mockCallback).toHaveBeenNthCalledWith(
90 | 4,
91 | 'debug',
92 | expect.stringContaining('Debug message')
93 | );
94 | });
95 |
96 | it('should respect log level with callback', () => {
97 | const mockCallback = vi.fn();
98 | const logger = new Logger({
99 | level: LogLevel.WARN,
100 | logCallback: mockCallback
101 | });
102 |
103 | logger.debug('Debug message');
104 | logger.info('Info message');
105 | logger.warn('Warning message');
106 | logger.error('Error message');
107 |
108 | // Only warn and error should be logged
109 | expect(mockCallback).toHaveBeenCalledTimes(2);
110 | expect(mockCallback).toHaveBeenNthCalledWith(
111 | 1,
112 | 'warn',
113 | expect.stringContaining('Warning message')
114 | );
115 | expect(mockCallback).toHaveBeenNthCalledWith(
116 | 2,
117 | 'error',
118 | expect.stringContaining('Error message')
119 | );
120 | });
121 |
122 | it('should handle raw log() calls with callback', () => {
123 | const mockCallback = vi.fn();
124 | const logger = new Logger({
125 | level: LogLevel.INFO,
126 | logCallback: mockCallback
127 | });
128 |
129 | logger.log('Raw message', 'with args');
130 |
131 | expect(mockCallback).toHaveBeenCalledWith('log', 'Raw message with args');
132 | });
133 | });
134 |
135 | describe('MCP mode with callback', () => {
136 | it('should not silence logs when mcpMode=true and callback is provided', () => {
137 | const mockCallback = vi.fn();
138 | const logger = new Logger({
139 | level: LogLevel.INFO,
140 | mcpMode: true,
141 | logCallback: mockCallback
142 | });
143 |
144 | logger.info('Test message');
145 |
146 | expect(mockCallback).toHaveBeenCalledWith(
147 | 'info',
148 | expect.stringContaining('Test message')
149 | );
150 | });
151 |
152 | it('should silence logs when mcpMode=true and no callback', () => {
153 | const consoleSpy = vi.spyOn(console, 'log');
154 | const logger = new Logger({
155 | level: LogLevel.INFO,
156 | mcpMode: true
157 | // No callback
158 | });
159 |
160 | logger.info('Test message');
161 |
162 | expect(consoleSpy).not.toHaveBeenCalled();
163 | consoleSpy.mockRestore();
164 | });
165 | });
166 |
167 | describe('Child loggers', () => {
168 | it('should inherit callback from parent', () => {
169 | const mockCallback = vi.fn();
170 | const parent = new Logger({
171 | level: LogLevel.INFO,
172 | logCallback: mockCallback
173 | });
174 |
175 | const child = parent.child('child');
176 | child.info('Child message');
177 |
178 | expect(mockCallback).toHaveBeenCalledWith(
179 | 'info',
180 | expect.stringContaining('[child]')
181 | );
182 | expect(mockCallback).toHaveBeenCalledWith(
183 | 'info',
184 | expect.stringContaining('Child message')
185 | );
186 | });
187 |
188 | it('should allow child to override callback', () => {
189 | const parentCallback = vi.fn();
190 | const childCallback = vi.fn();
191 |
192 | const parent = new Logger({
193 | level: LogLevel.INFO,
194 | logCallback: parentCallback
195 | });
196 |
197 | const child = parent.child('child', {
198 | logCallback: childCallback
199 | });
200 |
201 | parent.info('Parent message');
202 | child.info('Child message');
203 |
204 | expect(parentCallback).toHaveBeenCalledTimes(1);
205 | expect(childCallback).toHaveBeenCalledTimes(1);
206 | });
207 | });
208 |
209 | describe('Configuration updates', () => {
210 | it('should allow updating logCallback via setConfig', () => {
211 | const callback1 = vi.fn();
212 | const callback2 = vi.fn();
213 |
214 | const logger = new Logger({
215 | level: LogLevel.INFO,
216 | logCallback: callback1
217 | });
218 |
219 | logger.info('Message 1');
220 | expect(callback1).toHaveBeenCalledTimes(1);
221 |
222 | logger.setConfig({ logCallback: callback2 });
223 | logger.info('Message 2');
224 |
225 | expect(callback1).toHaveBeenCalledTimes(1);
226 | expect(callback2).toHaveBeenCalledTimes(1);
227 | });
228 |
229 | it('should maintain mcpMode behavior when updating config', () => {
230 | const callback = vi.fn();
231 | const logger = new Logger({
232 | level: LogLevel.INFO,
233 | mcpMode: true
234 | });
235 |
236 | // Initially silent (no callback)
237 | logger.info('Message 1');
238 | expect(callback).not.toHaveBeenCalled();
239 |
240 | // Add callback - should start logging
241 | logger.setConfig({ logCallback: callback });
242 | logger.info('Message 2');
243 | expect(callback).toHaveBeenCalledTimes(1);
244 | });
245 | });
246 |
247 | describe('Formatting with callback', () => {
248 | it('should include prefix in callback messages', () => {
249 | const mockCallback = vi.fn();
250 | const logger = new Logger({
251 | level: LogLevel.INFO,
252 | prefix: 'test-prefix',
253 | logCallback: mockCallback
254 | });
255 |
256 | logger.info('Test message');
257 |
258 | expect(mockCallback).toHaveBeenCalledWith(
259 | 'info',
260 | expect.stringContaining('[test-prefix]')
261 | );
262 | });
263 |
264 | it('should include timestamp when enabled', () => {
265 | const mockCallback = vi.fn();
266 | const logger = new Logger({
267 | level: LogLevel.INFO,
268 | timestamp: true,
269 | logCallback: mockCallback
270 | });
271 |
272 | logger.info('Test message');
273 |
274 | const [[, message]] = mockCallback.mock.calls;
275 | // Message should contain ISO timestamp pattern
276 | expect(message).toMatch(/\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
277 | });
278 |
279 | it('should format additional arguments', () => {
280 | const mockCallback = vi.fn();
281 | const logger = new Logger({
282 | level: LogLevel.INFO,
283 | logCallback: mockCallback
284 | });
285 |
286 | const data = { key: 'value' };
287 | logger.info('Test message', data, 'string arg');
288 |
289 | expect(mockCallback).toHaveBeenCalledWith(
290 | 'info',
291 | expect.stringContaining('Test message')
292 | );
293 | expect(mockCallback).toHaveBeenCalledWith(
294 | 'info',
295 | expect.stringContaining('"key"')
296 | );
297 | expect(mockCallback).toHaveBeenCalledWith(
298 | 'info',
299 | expect.stringContaining('string arg')
300 | );
301 | });
302 | });
303 |
304 | describe('Edge cases', () => {
305 | it('should handle null/undefined callback gracefully', () => {
306 | const logger = new Logger({
307 | level: LogLevel.INFO,
308 | logCallback: undefined
309 | });
310 |
311 | const consoleSpy = vi.spyOn(console, 'log');
312 |
313 | // Should fallback to console
314 | logger.info('Test message');
315 |
316 | expect(consoleSpy).toHaveBeenCalled();
317 | consoleSpy.mockRestore();
318 | });
319 |
320 | it('should not call callback when level is SILENT', () => {
321 | const mockCallback = vi.fn();
322 | const logger = new Logger({
323 | level: LogLevel.SILENT,
324 | logCallback: mockCallback
325 | });
326 |
327 | logger.error('Error');
328 | logger.warn('Warning');
329 | logger.info('Info');
330 | logger.debug('Debug');
331 |
332 | expect(mockCallback).not.toHaveBeenCalled();
333 | });
334 |
335 | it('should propagate callback errors', () => {
336 | const errorCallback: LogCallback = () => {
337 | throw new Error('Callback error');
338 | };
339 |
340 | const logger = new Logger({
341 | level: LogLevel.INFO,
342 | logCallback: errorCallback
343 | });
344 |
345 | // Should throw
346 | expect(() => {
347 | logger.info('Test message');
348 | }).toThrow('Callback error');
349 | });
350 | });
351 |
352 | describe('Environment variable detection', () => {
353 | it('should detect MCP mode from environment', () => {
354 | const originalEnv = process.env.MCP_MODE;
355 | process.env.MCP_MODE = 'true';
356 |
357 | const logger = new Logger({
358 | level: LogLevel.INFO
359 | });
360 |
361 | const config = logger.getConfig();
362 | expect(config.mcpMode).toBe(true);
363 | expect(config.silent).toBe(true); // Should be silent without callback
364 |
365 | // Cleanup
366 | if (originalEnv === undefined) {
367 | delete process.env.MCP_MODE;
368 | } else {
369 | process.env.MCP_MODE = originalEnv;
370 | }
371 | });
372 |
373 | it('should detect log level from environment', () => {
374 | const originalEnv = process.env.TASK_MASTER_LOG_LEVEL;
375 | process.env.TASK_MASTER_LOG_LEVEL = 'DEBUG';
376 |
377 | const logger = new Logger();
378 | const config = logger.getConfig();
379 | expect(config.level).toBe(LogLevel.DEBUG);
380 |
381 | // Cleanup
382 | if (originalEnv === undefined) {
383 | delete process.env.TASK_MASTER_LOG_LEVEL;
384 | } else {
385 | process.env.TASK_MASTER_LOG_LEVEL = originalEnv;
386 | }
387 | });
388 | });
389 | });
390 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/services/config-persistence.service.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Unit tests for ConfigPersistence service
3 | */
4 |
5 | import fs from 'node:fs/promises';
6 | import type { PartialConfiguration } from '@tm/core/common/interfaces/configuration.interface.js';
7 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8 | import { ConfigPersistence } from './config-persistence.service.js';
9 |
10 | vi.mock('node:fs', () => ({
11 | promises: {
12 | readFile: vi.fn(),
13 | writeFile: vi.fn(),
14 | mkdir: vi.fn(),
15 | unlink: vi.fn(),
16 | access: vi.fn(),
17 | readdir: vi.fn(),
18 | rename: vi.fn()
19 | }
20 | }));
21 |
22 | describe('ConfigPersistence', () => {
23 | let persistence: ConfigPersistence;
24 | const testProjectRoot = '/test/project';
25 |
26 | beforeEach(() => {
27 | persistence = new ConfigPersistence(testProjectRoot);
28 | vi.clearAllMocks();
29 | });
30 |
31 | afterEach(() => {
32 | vi.restoreAllMocks();
33 | });
34 |
35 | describe('saveConfig', () => {
36 | const mockConfig: PartialConfiguration = {
37 | models: { main: 'test-model', fallback: 'test-fallback' },
38 | storage: {
39 | type: 'file' as const,
40 | enableBackup: true,
41 | maxBackups: 5,
42 | enableCompression: true,
43 | encoding: 'utf-8',
44 | atomicOperations: true
45 | }
46 | };
47 |
48 | it('should save configuration to file', async () => {
49 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
50 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
51 |
52 | await persistence.saveConfig(mockConfig);
53 |
54 | expect(fs.mkdir).toHaveBeenCalledWith('/test/project/.taskmaster', {
55 | recursive: true
56 | });
57 |
58 | expect(fs.writeFile).toHaveBeenCalledWith(
59 | '/test/project/.taskmaster/config.json',
60 | JSON.stringify(mockConfig, null, 2),
61 | 'utf-8'
62 | );
63 | });
64 |
65 | it('should use atomic write when specified', async () => {
66 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
67 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
68 | vi.mocked(fs.rename).mockResolvedValue(undefined);
69 |
70 | await persistence.saveConfig(mockConfig, { atomic: true });
71 |
72 | // Should write to temp file first
73 | expect(fs.writeFile).toHaveBeenCalledWith(
74 | '/test/project/.taskmaster/config.json.tmp',
75 | JSON.stringify(mockConfig, null, 2),
76 | 'utf-8'
77 | );
78 |
79 | // Then rename to final location
80 | expect(fs.rename).toHaveBeenCalledWith(
81 | '/test/project/.taskmaster/config.json.tmp',
82 | '/test/project/.taskmaster/config.json'
83 | );
84 | });
85 |
86 | it('should create backup when requested', async () => {
87 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
88 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
89 | vi.mocked(fs.access).mockResolvedValue(undefined); // Config exists
90 | vi.mocked(fs.readFile).mockResolvedValue('{"old": "config"}');
91 | vi.mocked(fs.readdir).mockResolvedValue([]);
92 |
93 | await persistence.saveConfig(mockConfig, { createBackup: true });
94 |
95 | // Should create backup directory
96 | expect(fs.mkdir).toHaveBeenCalledWith(
97 | '/test/project/.taskmaster/backups',
98 | { recursive: true }
99 | );
100 |
101 | // Should read existing config for backup
102 | expect(fs.readFile).toHaveBeenCalledWith(
103 | '/test/project/.taskmaster/config.json',
104 | 'utf-8'
105 | );
106 |
107 | // Should write backup file
108 | expect(fs.writeFile).toHaveBeenCalledWith(
109 | expect.stringContaining('/test/project/.taskmaster/backups/config-'),
110 | '{"old": "config"}',
111 | 'utf-8'
112 | );
113 | });
114 |
115 | it('should not create backup if config does not exist', async () => {
116 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
117 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
118 | vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
119 |
120 | await persistence.saveConfig(mockConfig, { createBackup: true });
121 |
122 | // Should not read or create backup
123 | expect(fs.readFile).not.toHaveBeenCalled();
124 | expect(fs.writeFile).toHaveBeenCalledTimes(1); // Only the main config
125 | });
126 |
127 | it('should throw TaskMasterError on save failure', async () => {
128 | vi.mocked(fs.mkdir).mockRejectedValue(new Error('Disk full'));
129 |
130 | await expect(persistence.saveConfig(mockConfig)).rejects.toThrow(
131 | 'Failed to save configuration'
132 | );
133 | });
134 | });
135 |
136 | describe('configExists', () => {
137 | it('should return true when config exists', async () => {
138 | vi.mocked(fs.access).mockResolvedValue(undefined);
139 |
140 | const exists = await persistence.configExists();
141 |
142 | expect(fs.access).toHaveBeenCalledWith(
143 | '/test/project/.taskmaster/config.json'
144 | );
145 | expect(exists).toBe(true);
146 | });
147 |
148 | it('should return false when config does not exist', async () => {
149 | vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
150 |
151 | const exists = await persistence.configExists();
152 |
153 | expect(exists).toBe(false);
154 | });
155 | });
156 |
157 | describe('deleteConfig', () => {
158 | it('should delete configuration file', async () => {
159 | vi.mocked(fs.unlink).mockResolvedValue(undefined);
160 |
161 | await persistence.deleteConfig();
162 |
163 | expect(fs.unlink).toHaveBeenCalledWith(
164 | '/test/project/.taskmaster/config.json'
165 | );
166 | });
167 |
168 | it('should not throw when file does not exist', async () => {
169 | const error = new Error('File not found') as any;
170 | error.code = 'ENOENT';
171 | vi.mocked(fs.unlink).mockRejectedValue(error);
172 |
173 | await expect(persistence.deleteConfig()).resolves.not.toThrow();
174 | });
175 |
176 | it('should throw TaskMasterError for other errors', async () => {
177 | vi.mocked(fs.unlink).mockRejectedValue(new Error('Permission denied'));
178 |
179 | await expect(persistence.deleteConfig()).rejects.toThrow(
180 | 'Failed to delete configuration'
181 | );
182 | });
183 | });
184 |
185 | describe('getBackups', () => {
186 | it('should return list of backup files sorted newest first', async () => {
187 | vi.mocked(fs.readdir).mockResolvedValue([
188 | 'config-2024-01-01T10-00-00-000Z.json',
189 | 'config-2024-01-02T10-00-00-000Z.json',
190 | 'config-2024-01-03T10-00-00-000Z.json',
191 | 'other-file.txt'
192 | ] as any);
193 |
194 | const backups = await persistence.getBackups();
195 |
196 | expect(fs.readdir).toHaveBeenCalledWith(
197 | '/test/project/.taskmaster/backups'
198 | );
199 |
200 | expect(backups).toEqual([
201 | 'config-2024-01-03T10-00-00-000Z.json',
202 | 'config-2024-01-02T10-00-00-000Z.json',
203 | 'config-2024-01-01T10-00-00-000Z.json'
204 | ]);
205 | });
206 |
207 | it('should return empty array when backup directory does not exist', async () => {
208 | vi.mocked(fs.readdir).mockRejectedValue(new Error('Not found'));
209 |
210 | const backups = await persistence.getBackups();
211 |
212 | expect(backups).toEqual([]);
213 | });
214 |
215 | it('should filter out non-backup files', async () => {
216 | vi.mocked(fs.readdir).mockResolvedValue([
217 | 'config-2024-01-01T10-00-00-000Z.json',
218 | 'README.md',
219 | '.DS_Store',
220 | 'config.json',
221 | 'config-backup.json' // Wrong format
222 | ] as any);
223 |
224 | const backups = await persistence.getBackups();
225 |
226 | expect(backups).toEqual(['config-2024-01-01T10-00-00-000Z.json']);
227 | });
228 | });
229 |
230 | describe('restoreFromBackup', () => {
231 | const backupFile = 'config-2024-01-01T10-00-00-000Z.json';
232 | const backupContent = '{"restored": "config"}';
233 |
234 | it('should restore configuration from backup', async () => {
235 | vi.mocked(fs.readFile).mockResolvedValue(backupContent);
236 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
237 |
238 | await persistence.restoreFromBackup(backupFile);
239 |
240 | expect(fs.readFile).toHaveBeenCalledWith(
241 | `/test/project/.taskmaster/backups/${backupFile}`,
242 | 'utf-8'
243 | );
244 |
245 | expect(fs.writeFile).toHaveBeenCalledWith(
246 | '/test/project/.taskmaster/config.json',
247 | backupContent,
248 | 'utf-8'
249 | );
250 | });
251 |
252 | it('should throw TaskMasterError when backup file not found', async () => {
253 | vi.mocked(fs.readFile).mockRejectedValue(new Error('File not found'));
254 |
255 | await expect(
256 | persistence.restoreFromBackup('nonexistent.json')
257 | ).rejects.toThrow('Failed to restore from backup');
258 | });
259 |
260 | it('should throw TaskMasterError on write failure', async () => {
261 | vi.mocked(fs.readFile).mockResolvedValue(backupContent);
262 | vi.mocked(fs.writeFile).mockRejectedValue(new Error('Disk full'));
263 |
264 | await expect(persistence.restoreFromBackup(backupFile)).rejects.toThrow(
265 | 'Failed to restore from backup'
266 | );
267 | });
268 | });
269 |
270 | describe('backup management', () => {
271 | it('should clean old backups when limit exceeded', async () => {
272 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
273 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
274 | vi.mocked(fs.access).mockResolvedValue(undefined);
275 | vi.mocked(fs.readFile).mockResolvedValue('{"old": "config"}');
276 | vi.mocked(fs.unlink).mockResolvedValue(undefined);
277 |
278 | // Mock 7 existing backups
279 | vi.mocked(fs.readdir).mockResolvedValue([
280 | 'config-2024-01-01T10-00-00-000Z.json',
281 | 'config-2024-01-02T10-00-00-000Z.json',
282 | 'config-2024-01-03T10-00-00-000Z.json',
283 | 'config-2024-01-04T10-00-00-000Z.json',
284 | 'config-2024-01-05T10-00-00-000Z.json',
285 | 'config-2024-01-06T10-00-00-000Z.json',
286 | 'config-2024-01-07T10-00-00-000Z.json'
287 | ] as any);
288 |
289 | await persistence.saveConfig({}, { createBackup: true });
290 |
291 | // Should delete oldest backups (keeping 5)
292 | expect(fs.unlink).toHaveBeenCalledWith(
293 | '/test/project/.taskmaster/backups/config-2024-01-01T10-00-00-000Z.json'
294 | );
295 | expect(fs.unlink).toHaveBeenCalledWith(
296 | '/test/project/.taskmaster/backups/config-2024-01-02T10-00-00-000Z.json'
297 | );
298 | });
299 |
300 | it('should handle backup cleanup errors gracefully', async () => {
301 | vi.mocked(fs.mkdir).mockResolvedValue(undefined);
302 | vi.mocked(fs.writeFile).mockResolvedValue(undefined);
303 | vi.mocked(fs.access).mockResolvedValue(undefined);
304 | vi.mocked(fs.readFile).mockResolvedValue('{"old": "config"}');
305 | vi.mocked(fs.readdir).mockResolvedValue(['config-old.json'] as any);
306 | vi.mocked(fs.unlink).mockRejectedValue(new Error('Permission denied'));
307 |
308 | // Mock console.warn to verify it's called
309 | const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
310 |
311 | // Should not throw even if cleanup fails
312 | await expect(
313 | persistence.saveConfig({}, { createBackup: true })
314 | ).resolves.not.toThrow();
315 |
316 | expect(warnSpy).toHaveBeenCalledWith(
317 | 'Failed to clean old backups:',
318 | expect.any(Error)
319 | );
320 |
321 | warnSpy.mockRestore();
322 | });
323 | });
324 | });
325 |
```