This is page 26 of 50. Use http://codebase.md/eyaltoledano/claude-task-master?lines=false&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/expand-all-tasks.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* Tests for the expand-all-tasks.js module
*/
import { jest } from '@jest/globals';
// Mock the dependencies before importing the module under test
jest.unstable_mockModule(
'../../../../../scripts/modules/task-manager/expand-task.js',
() => ({
default: jest.fn()
})
);
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
readJSON: jest.fn(),
log: jest.fn(),
isSilentMode: jest.fn(() => false),
findProjectRoot: jest.fn(() => '/test/project'),
aggregateTelemetry: jest.fn()
}));
jest.unstable_mockModule(
'../../../../../scripts/modules/config-manager.js',
() => ({
getDebugFlag: jest.fn(() => false)
})
);
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
startLoadingIndicator: jest.fn(),
stopLoadingIndicator: jest.fn(),
displayAiUsageSummary: jest.fn()
}));
jest.unstable_mockModule('chalk', () => ({
default: {
white: { bold: jest.fn((text) => text) },
cyan: jest.fn((text) => text),
green: jest.fn((text) => text),
gray: jest.fn((text) => text),
red: jest.fn((text) => text),
bold: jest.fn((text) => text)
}
}));
jest.unstable_mockModule('boxen', () => ({
default: jest.fn((text) => text)
}));
// Import the mocked modules
const { default: expandTask } = await import(
'../../../../../scripts/modules/task-manager/expand-task.js'
);
const { readJSON, aggregateTelemetry, findProjectRoot } = await import(
'../../../../../scripts/modules/utils.js'
);
// Import the module under test
const { default: expandAllTasks } = await import(
'../../../../../scripts/modules/task-manager/expand-all-tasks.js'
);
const mockExpandTask = expandTask;
const mockReadJSON = readJSON;
const mockAggregateTelemetry = aggregateTelemetry;
const mockFindProjectRoot = findProjectRoot;
describe('expandAllTasks', () => {
const mockTasksPath = '/test/tasks.json';
const mockProjectRoot = '/test/project';
const mockSession = { userId: 'test-user' };
const mockMcpLog = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn()
};
const sampleTasksData = {
tag: 'master',
tasks: [
{
id: 1,
title: 'Pending Task 1',
status: 'pending',
subtasks: []
},
{
id: 2,
title: 'In Progress Task',
status: 'in-progress',
subtasks: []
},
{
id: 3,
title: 'Done Task',
status: 'done',
subtasks: []
},
{
id: 4,
title: 'Task with Subtasks',
status: 'pending',
subtasks: [{ id: '4.1', title: 'Existing subtask' }]
}
]
};
beforeEach(() => {
jest.clearAllMocks();
mockReadJSON.mockReturnValue(sampleTasksData);
mockAggregateTelemetry.mockReturnValue({
timestamp: '2024-01-01T00:00:00.000Z',
commandName: 'expand-all-tasks',
totalCost: 0.1,
totalTokens: 2000,
inputTokens: 1200,
outputTokens: 800
});
});
describe('successful expansion', () => {
test('should expand all eligible pending tasks', async () => {
// Arrange
const mockTelemetryData = {
timestamp: '2024-01-01T00:00:00.000Z',
commandName: 'expand-task',
totalCost: 0.05,
totalTokens: 1000
};
mockExpandTask.mockResolvedValue({
telemetryData: mockTelemetryData
});
// Act
const result = await expandAllTasks(
mockTasksPath,
3, // numSubtasks
false, // useResearch
'test context', // additionalContext
false, // force
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
},
'json' // outputFormat
);
// Assert
expect(result.success).toBe(true);
expect(result.expandedCount).toBe(2); // Tasks 1 and 2 (pending and in-progress)
expect(result.failedCount).toBe(0);
expect(result.skippedCount).toBe(0);
expect(result.tasksToExpand).toBe(2);
expect(result.telemetryData).toBeDefined();
// Verify readJSON was called correctly
expect(mockReadJSON).toHaveBeenCalledWith(
mockTasksPath,
mockProjectRoot,
'master'
);
// Verify expandTask was called for eligible tasks
expect(mockExpandTask).toHaveBeenCalledTimes(2);
expect(mockExpandTask).toHaveBeenCalledWith(
mockTasksPath,
1,
3,
false,
'test context',
expect.objectContaining({
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
}),
false
);
});
test('should handle force flag to expand tasks with existing subtasks', async () => {
// Arrange
mockExpandTask.mockResolvedValue({
telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
});
// Act
const result = await expandAllTasks(
mockTasksPath,
2,
false,
'',
true, // force = true
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
},
'json'
);
// Assert
expect(result.expandedCount).toBe(3); // Tasks 1, 2, and 4 (including task with existing subtasks)
expect(mockExpandTask).toHaveBeenCalledTimes(3);
});
test('should handle research flag', async () => {
// Arrange
mockExpandTask.mockResolvedValue({
telemetryData: { commandName: 'expand-task', totalCost: 0.08 }
});
// Act
const result = await expandAllTasks(
mockTasksPath,
undefined, // numSubtasks not specified
true, // useResearch = true
'research context',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
},
'json'
);
// Assert
expect(result.success).toBe(true);
expect(mockExpandTask).toHaveBeenCalledWith(
mockTasksPath,
expect.any(Number),
undefined,
true, // research flag passed correctly
'research context',
expect.any(Object),
false
);
});
test('should pass complexityReportPath to expandTask when provided in context', async () => {
// Arrange
const mockComplexityReportPath =
'/test/project/.taskmaster/reports/task-complexity-report.json';
mockExpandTask.mockResolvedValue({
telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
});
// Act
const result = await expandAllTasks(
mockTasksPath,
undefined, // numSubtasks not specified, should use complexity report
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master',
complexityReportPath: mockComplexityReportPath
},
'json'
);
// Assert
expect(result.success).toBe(true);
expect(result.expandedCount).toBe(2); // Tasks 1 and 2
// Verify expandTask was called with complexityReportPath in context
expect(mockExpandTask).toHaveBeenCalledWith(
mockTasksPath,
expect.any(Number), // task id
undefined, // numSubtasks
false, // useResearch
'', // additionalContext
expect.objectContaining({
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master',
complexityReportPath: mockComplexityReportPath
}),
false // force
);
});
test('should return success with message when no tasks are eligible', async () => {
// Arrange - Mock tasks data with no eligible tasks
const noEligibleTasksData = {
tag: 'master',
tasks: [
{ id: 1, status: 'done', subtasks: [] },
{
id: 2,
status: 'pending',
subtasks: [{ id: '2.1', title: 'existing' }]
}
]
};
mockReadJSON.mockReturnValue(noEligibleTasksData);
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false, // force = false, so task with subtasks won't be expanded
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
},
'json'
);
// Assert
expect(result.success).toBe(true);
expect(result.expandedCount).toBe(0);
expect(result.failedCount).toBe(0);
expect(result.skippedCount).toBe(0);
expect(result.tasksToExpand).toBe(0);
expect(result.message).toBe('No tasks eligible for expansion.');
expect(mockExpandTask).not.toHaveBeenCalled();
});
});
describe('error handling', () => {
test('should handle expandTask failures gracefully', async () => {
// Arrange
mockExpandTask
.mockResolvedValueOnce({ telemetryData: { totalCost: 0.05 } }) // First task succeeds
.mockRejectedValueOnce(new Error('AI service error')); // Second task fails
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
},
'json'
);
// Assert
expect(result.success).toBe(true);
expect(result.expandedCount).toBe(1);
expect(result.failedCount).toBe(1);
});
test('should throw error when tasks.json is invalid', async () => {
// Arrange
mockReadJSON.mockReturnValue(null);
// Act & Assert
await expect(
expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
},
'json'
)
).rejects.toThrow('Invalid tasks data');
});
test('should throw error when project root cannot be determined', async () => {
// Arrange - Mock findProjectRoot to return null for this test
mockFindProjectRoot.mockReturnValueOnce(null);
// Act & Assert
await expect(
expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
tag: 'master'
// No projectRoot provided, and findProjectRoot will return null
},
'json'
)
).rejects.toThrow('Could not determine project root directory');
});
});
describe('telemetry aggregation', () => {
test('should aggregate telemetry data from multiple expand operations', async () => {
// Arrange
const telemetryData1 = {
commandName: 'expand-task',
totalCost: 0.03,
totalTokens: 600
};
const telemetryData2 = {
commandName: 'expand-task',
totalCost: 0.04,
totalTokens: 800
};
mockExpandTask
.mockResolvedValueOnce({ telemetryData: telemetryData1 })
.mockResolvedValueOnce({ telemetryData: telemetryData2 });
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
},
'json'
);
// Assert
expect(mockAggregateTelemetry).toHaveBeenCalledWith(
[telemetryData1, telemetryData2],
'expand-all-tasks'
);
expect(result.telemetryData).toBeDefined();
expect(result.telemetryData.commandName).toBe('expand-all-tasks');
});
test('should handle missing telemetry data gracefully', async () => {
// Arrange
mockExpandTask.mockResolvedValue({}); // No telemetryData
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'master'
},
'json'
);
// Assert
expect(result.success).toBe(true);
expect(mockAggregateTelemetry).toHaveBeenCalledWith(
[],
'expand-all-tasks'
);
});
});
describe('output format handling', () => {
test('should use text output format for CLI calls', async () => {
// Arrange
mockExpandTask.mockResolvedValue({
telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
});
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
projectRoot: mockProjectRoot,
tag: 'master'
// No mcpLog provided, should use CLI logger
},
'text' // CLI output format
);
// Assert
expect(result.success).toBe(true);
// In text mode, loading indicators and console output would be used
// This is harder to test directly but we can verify the result structure
});
test('should handle context tag properly', async () => {
// Arrange
const taggedTasksData = {
...sampleTasksData,
tag: 'feature-branch'
};
mockReadJSON.mockReturnValue(taggedTasksData);
mockExpandTask.mockResolvedValue({
telemetryData: { commandName: 'expand-task', totalCost: 0.05 }
});
// Act
const result = await expandAllTasks(
mockTasksPath,
3,
false,
'',
false,
{
session: mockSession,
mcpLog: mockMcpLog,
projectRoot: mockProjectRoot,
tag: 'feature-branch'
},
'json'
);
// Assert
expect(mockReadJSON).toHaveBeenCalledWith(
mockTasksPath,
mockProjectRoot,
'feature-branch'
);
expect(mockExpandTask).toHaveBeenCalledWith(
mockTasksPath,
expect.any(Number),
3,
false,
'',
expect.objectContaining({
tag: 'feature-branch'
}),
false
);
});
});
});
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/integration/services/export.service.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Export Service
* Core service for exporting tasks to external systems (e.g., Hamster briefs)
*/
import {
ERROR_CODES,
TaskMasterError
} from '../../../common/errors/task-master-error.js';
import type { Task, TaskStatus } from '../../../common/types/index.js';
import { AuthManager } from '../../auth/managers/auth-manager.js';
import type { UserContext } from '../../auth/types.js';
import { ConfigManager } from '../../config/managers/config-manager.js';
import { FileStorage } from '../../storage/adapters/file-storage/index.js';
// Type definitions for the bulk API response
interface TaskImportResult {
externalId?: string;
index: number;
success: boolean;
taskId?: string;
error?: string;
validationErrors?: string[];
}
interface BulkTasksResponse {
dryRun: boolean;
totalTasks: number;
successCount: number;
failedCount: number;
skippedCount: number;
results: TaskImportResult[];
summary: {
message: string;
duration: number;
};
}
/**
* Options for exporting tasks
*/
export interface ExportTasksOptions {
/** Optional tag to export tasks from (uses active tag if not provided) */
tag?: string;
/** Brief ID to export to */
briefId?: string;
/** Organization ID (required if briefId is provided) */
orgId?: string;
/** Filter by task status */
status?: TaskStatus;
/** Exclude subtasks from export (default: false, subtasks included by default) */
excludeSubtasks?: boolean;
}
/**
* Result of the export operation
*/
export interface ExportResult {
/** Whether the export was successful */
success: boolean;
/** Number of tasks exported */
taskCount: number;
/** The brief ID tasks were exported to */
briefId: string;
/** The organization ID */
orgId: string;
/** Optional message */
message?: string;
/** Error details if export failed */
error?: {
code: string;
message: string;
};
}
/**
* Brief information from API
*/
export interface Brief {
id: string;
accountId: string;
createdAt: string;
name?: string;
}
/**
* ExportService handles task export to external systems
*/
export class ExportService {
private configManager: ConfigManager;
private authManager: AuthManager;
constructor(configManager: ConfigManager, authManager: AuthManager) {
this.configManager = configManager;
this.authManager = authManager;
}
/**
* Export tasks to a brief
*/
async exportTasks(options: ExportTasksOptions): Promise<ExportResult> {
const isAuthenticated = await this.authManager.hasValidSession();
// Validate authentication
if (!isAuthenticated) {
throw new TaskMasterError(
'Authentication required for export',
ERROR_CODES.AUTHENTICATION_ERROR
);
}
// Get current context
const context = await this.authManager.getContext();
// Determine org and brief IDs
let orgId = options.orgId || context?.orgId;
let briefId = options.briefId || context?.briefId;
// Validate we have necessary IDs
if (!orgId) {
throw new TaskMasterError(
'Organization ID is required for export. Use "tm context org" to select one.',
ERROR_CODES.MISSING_CONFIGURATION
);
}
if (!briefId) {
throw new TaskMasterError(
'Brief ID is required for export. Use "tm context brief" or provide --brief flag.',
ERROR_CODES.MISSING_CONFIGURATION
);
}
// Get tasks from the specified or active tag
const activeTag = this.configManager.getActiveTag();
const tag = options.tag || activeTag;
// Always read tasks from local file storage for export
// (we're exporting local tasks to a remote brief)
const fileStorage = new FileStorage(this.configManager.getProjectRoot());
await fileStorage.initialize();
// Load tasks with filters applied at storage layer
const filteredTasks = await fileStorage.loadTasks(tag, {
status: options.status,
excludeSubtasks: options.excludeSubtasks
});
// Get total count (without filters) for comparison
const allTasks = await fileStorage.loadTasks(tag);
const taskListResult = {
tasks: filteredTasks,
total: allTasks.length,
filtered: filteredTasks.length,
tag,
storageType: 'file' as const
};
if (taskListResult.tasks.length === 0) {
return {
success: false,
taskCount: 0,
briefId,
orgId,
message: 'No tasks found to export',
error: {
code: 'NO_TASKS',
message: 'No tasks match the specified criteria'
}
};
}
try {
// Call the export API with the original tasks
// performExport will handle the transformation based on the method used
await this.performExport(orgId, briefId, taskListResult.tasks);
return {
success: true,
taskCount: taskListResult.tasks.length,
briefId,
orgId,
message: `Successfully exported ${taskListResult.tasks.length} task(s) to brief`
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
return {
success: false,
taskCount: 0,
briefId,
orgId,
error: {
code: 'EXPORT_FAILED',
message: errorMessage
}
};
}
}
/**
* Export tasks from a brief ID or URL
*/
async exportFromBriefInput(briefInput: string): Promise<ExportResult> {
// Extract brief ID from input
const briefId = this.extractBriefId(briefInput);
if (!briefId) {
throw new TaskMasterError(
'Invalid brief ID or URL provided',
ERROR_CODES.VALIDATION_ERROR
);
}
// Fetch brief to get organization
const brief = await this.authManager.getBrief(briefId);
if (!brief) {
throw new TaskMasterError(
'Brief not found or you do not have access',
ERROR_CODES.NOT_FOUND
);
}
// Export with the resolved org and brief
return this.exportTasks({
orgId: brief.accountId,
briefId: brief.id
});
}
/**
* Validate export context before prompting
*/
async validateContext(): Promise<{
hasOrg: boolean;
hasBrief: boolean;
context: UserContext | null;
}> {
const context = await this.authManager.getContext();
return {
hasOrg: !!context?.orgId,
hasBrief: !!context?.briefId,
context
};
}
/**
* Transform tasks for API bulk import format (flat structure)
*/
private transformTasksForBulkImport(tasks: Task[]): any[] {
const flatTasks: any[] = [];
// Process each task and its subtasks
tasks.forEach((task) => {
// Add parent task
flatTasks.push({
externalId: String(task.id),
title: task.title,
description: this.enrichDescription(task),
status: this.mapStatusForAPI(task.status),
priority: task.priority || 'medium',
dependencies: task.dependencies?.map(String) || [],
details: task.details,
testStrategy: task.testStrategy,
complexity: task.complexity,
metadata: {
complexity: task.complexity,
originalId: task.id,
originalDescription: task.description,
originalDetails: task.details,
originalTestStrategy: task.testStrategy
}
});
// Add subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach((subtask) => {
flatTasks.push({
externalId: `${task.id}.${subtask.id}`,
parentExternalId: String(task.id),
title: subtask.title,
description: this.enrichDescription(subtask),
status: this.mapStatusForAPI(subtask.status),
priority: subtask.priority || 'medium',
dependencies:
subtask.dependencies?.map((dep) => {
// Convert subtask dependencies to full ID format
if (String(dep).includes('.')) {
return String(dep);
}
return `${task.id}.${dep}`;
}) || [],
details: subtask.details,
testStrategy: subtask.testStrategy,
complexity: subtask.complexity,
metadata: {
complexity: subtask.complexity,
originalId: subtask.id,
originalDescription: subtask.description,
originalDetails: subtask.details,
originalTestStrategy: subtask.testStrategy
}
});
});
}
});
return flatTasks;
}
/**
* Enrich task/subtask description with implementation details and test strategy
* Creates a comprehensive markdown-formatted description
*/
private enrichDescription(taskOrSubtask: Task | any): string {
const sections: string[] = [];
// Start with original description if it exists
if (taskOrSubtask.description) {
sections.push(taskOrSubtask.description);
}
// Add implementation details section
if (taskOrSubtask.details) {
sections.push('## Implementation Details\n');
sections.push(taskOrSubtask.details);
}
// Add test strategy section
if (taskOrSubtask.testStrategy) {
sections.push('## Test Strategy\n');
sections.push(taskOrSubtask.testStrategy);
}
// Join sections with double newlines for better markdown formatting
return sections.join('\n\n').trim() || 'No description provided';
}
/**
* Map internal status to API status format
*/
private mapStatusForAPI(status?: string): string {
switch (status) {
case 'pending':
return 'todo';
case 'in-progress':
return 'in_progress';
case 'done':
return 'done';
default:
return 'todo';
}
}
/**
* Perform the actual export API call
*/
private async performExport(
orgId: string,
briefId: string,
tasks: any[]
): Promise<void> {
// Check if we should use the API endpoint or direct Supabase
const apiEndpoint =
process.env.TM_BASE_DOMAIN || process.env.TM_PUBLIC_BASE_DOMAIN;
if (apiEndpoint) {
// Use the new bulk import API endpoint
const apiUrl = `${apiEndpoint}/ai/api/v1/briefs/${briefId}/tasks`;
// Transform tasks to flat structure for API
const flatTasks = this.transformTasksForBulkImport(tasks);
// Prepare request body
const requestBody = {
source: 'task-master-cli',
options: {
dryRun: false,
stopOnError: false
},
accountId: orgId,
tasks: flatTasks
};
// Get auth token
const accessToken = await this.authManager.getAccessToken();
if (!accessToken) {
throw new Error('Not authenticated');
}
// Make API request
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`
},
body: JSON.stringify(requestBody)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`API request failed: ${response.status} - ${errorText}`
);
}
const result = (await response.json()) as BulkTasksResponse;
if (result.failedCount > 0) {
const failedTasks = result.results
.filter((r) => !r.success)
.map((r) => `${r.externalId}: ${r.error}`)
.join(', ');
console.warn(
`Warning: ${result.failedCount} tasks failed to import: ${failedTasks}`
);
}
console.log(
`Successfully exported ${result.successCount} of ${result.totalTasks} tasks to brief ${briefId}`
);
} else {
// Direct Supabase approach is no longer supported
// The extractTasks method has been removed from SupabaseRepository
// as we now exclusively use the API endpoint for exports
throw new Error(
'Export API endpoint not configured. Please set TM_PUBLIC_BASE_DOMAIN environment variable to enable task export.'
);
}
}
/**
* Extract a brief ID from raw input (ID or URL)
*/
private extractBriefId(input: string): string | null {
const raw = input?.trim() ?? '';
if (!raw) return null;
const parseUrl = (s: string): URL | null => {
try {
return new URL(s);
} catch {}
try {
return new URL(`https://${s}`);
} catch {}
return null;
};
const fromParts = (path: string): string | null => {
const parts = path.split('/').filter(Boolean);
const briefsIdx = parts.lastIndexOf('briefs');
const candidate =
briefsIdx >= 0 && parts.length > briefsIdx + 1
? parts[briefsIdx + 1]
: parts[parts.length - 1];
return candidate?.trim() || null;
};
// Try to parse as URL
const url = parseUrl(raw);
if (url) {
const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
const candidate = (qId || fromParts(url.pathname)) ?? null;
if (candidate) {
if (this.isLikelyId(candidate) || candidate.length >= 8) {
return candidate;
}
}
}
// Check if it looks like a path without scheme
if (raw.includes('/')) {
const candidate = fromParts(raw);
if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) {
return candidate;
}
}
// Return as-is if it looks like an ID
if (this.isLikelyId(raw) || raw.length >= 8) {
return raw;
}
return null;
}
/**
* Check if a string looks like a brief ID (UUID-like)
*/
private isLikelyId(value: string): boolean {
const uuidRegex =
/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i;
const slugRegex = /^[A-Za-z0-9_-]{16,}$/;
return (
uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value)
);
}
}
```
--------------------------------------------------------------------------------
/scripts/modules/task-manager/expand-task.js:
--------------------------------------------------------------------------------
```javascript
import fs from 'fs';
import { readJSON, writeJSON } from '../utils.js';
import {
displayAiUsageSummary,
startLoadingIndicator,
stopLoadingIndicator
} from '../ui.js';
import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
import { generateObjectService } from '../ai-services-unified.js';
import {
getDefaultSubtasks,
hasCodebaseAnalysis,
getDebugFlag
} from '../config-manager.js';
import { getPromptManager } from '../prompt-manager.js';
import { findProjectRoot, flattenTasksWithSubtasks } from '../utils.js';
import { ContextGatherer } from '../utils/contextGatherer.js';
import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
import { tryExpandViaRemote } from '@tm/bridge';
import { createBridgeLogger } from '../bridge-utils.js';
/**
* Expand a task into subtasks using the unified AI service (generateObjectService).
* Appends new subtasks by default. Replaces existing subtasks if force=true.
* Integrates complexity report to determine subtask count and prompt if available,
* unless numSubtasks is explicitly provided.
* @param {string} tasksPath - Path to the tasks.json file
* @param {number} taskId - Task ID to expand
* @param {number | null | undefined} [numSubtasks] - Optional: Explicit target number of subtasks. If null/undefined, check complexity report or config default.
* @param {boolean} [useResearch=false] - Whether to use the research AI role.
* @param {string} [additionalContext=''] - Optional additional context.
* @param {Object} context - Context object containing session and mcpLog.
* @param {Object} [context.session] - Session object from MCP.
* @param {Object} [context.mcpLog] - MCP logger object.
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
* @param {boolean} [force=false] - If true, replace existing subtasks; otherwise, append.
* @returns {Promise<Object>} The updated parent task object with new subtasks.
* @throws {Error} If task not found, AI service fails, or parsing fails.
*/
async function expandTask(
tasksPath,
taskId,
numSubtasks,
useResearch = false,
additionalContext = '',
context = {},
force = false
) {
const {
session,
mcpLog,
projectRoot: contextProjectRoot,
tag,
complexityReportPath
} = context;
const outputFormat = mcpLog ? 'json' : 'text';
// Determine projectRoot: Use from context if available, otherwise derive from tasksPath
const projectRoot = contextProjectRoot || findProjectRoot(tasksPath);
// Create unified logger and report function
const { logger, report, isMCP } = createBridgeLogger(mcpLog, session);
if (isMCP) {
logger.info(`expandTask called with context: session=${!!session}`);
}
try {
// --- BRIDGE: Try remote expansion first (API storage) ---
const remoteResult = await tryExpandViaRemote({
taskId,
numSubtasks,
useResearch,
additionalContext,
force,
projectRoot,
tag,
isMCP,
outputFormat,
report
});
// If remote handled it, return the result
if (remoteResult) {
return remoteResult;
}
// Otherwise fall through to file-based logic below
// --- End BRIDGE ---
// --- Task Loading/Filtering (Unchanged) ---
logger.info(`Reading tasks from ${tasksPath}`);
const data = readJSON(tasksPath, projectRoot, tag);
if (!data || !data.tasks)
throw new Error(`Invalid tasks data in ${tasksPath}`);
const taskIndex = data.tasks.findIndex(
(t) => t.id === parseInt(taskId, 10)
);
if (taskIndex === -1) throw new Error(`Task ${taskId} not found`);
const task = data.tasks[taskIndex];
logger.info(
`Expanding task ${taskId}: ${task.title}${useResearch ? ' with research' : ''}`
);
// --- End Task Loading/Filtering ---
// --- Handle Force Flag: Clear existing subtasks if force=true ---
if (force && Array.isArray(task.subtasks) && task.subtasks.length > 0) {
logger.info(
`Force flag set. Clearing existing ${task.subtasks.length} subtasks for task ${taskId}.`
);
task.subtasks = []; // Clear existing subtasks
}
// --- End Force Flag Handling ---
// --- Context Gathering ---
let gatheredContext = '';
try {
const contextGatherer = new ContextGatherer(projectRoot, tag);
const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'expand-task');
const searchQuery = `${task.title} ${task.description}`;
const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
maxResults: 5,
includeSelf: true
});
const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
const finalTaskIds = [
...new Set([taskId.toString(), ...relevantTaskIds])
];
if (finalTaskIds.length > 0) {
const contextResult = await contextGatherer.gather({
tasks: finalTaskIds,
format: 'research'
});
gatheredContext = contextResult.context || '';
}
} catch (contextError) {
logger.warn(`Could not gather context: ${contextError.message}`);
}
// --- End Context Gathering ---
// --- Complexity Report Integration ---
let finalSubtaskCount;
let complexityReasoningContext = '';
let taskAnalysis = null;
logger.info(
`Looking for complexity report at: ${complexityReportPath}${tag !== 'master' ? ` (tag-specific for '${tag}')` : ''}`
);
try {
if (fs.existsSync(complexityReportPath)) {
const complexityReport = readJSON(complexityReportPath);
taskAnalysis = complexityReport?.complexityAnalysis?.find(
(a) => a.taskId === task.id
);
if (taskAnalysis) {
logger.info(
`Found complexity analysis for task ${task.id}: Score ${taskAnalysis.complexityScore}`
);
if (taskAnalysis.reasoning) {
complexityReasoningContext = `\nComplexity Analysis Reasoning: ${taskAnalysis.reasoning}`;
}
} else {
logger.info(
`No complexity analysis found for task ${task.id} in report.`
);
}
} else {
logger.info(
`Complexity report not found at ${complexityReportPath}. Skipping complexity check.`
);
}
} catch (reportError) {
logger.warn(
`Could not read or parse complexity report: ${reportError.message}. Proceeding without it.`
);
}
// Determine final subtask count
const explicitNumSubtasks = parseInt(numSubtasks, 10);
if (!Number.isNaN(explicitNumSubtasks) && explicitNumSubtasks >= 0) {
finalSubtaskCount = explicitNumSubtasks;
logger.info(
`Using explicitly provided subtask count: ${finalSubtaskCount}`
);
} else if (taskAnalysis?.recommendedSubtasks) {
finalSubtaskCount = parseInt(taskAnalysis.recommendedSubtasks, 10);
logger.info(
`Using subtask count from complexity report: ${finalSubtaskCount}`
);
} else {
finalSubtaskCount = getDefaultSubtasks(session);
logger.info(`Using default number of subtasks: ${finalSubtaskCount}`);
}
if (Number.isNaN(finalSubtaskCount) || finalSubtaskCount < 0) {
logger.warn(
`Invalid subtask count determined (${finalSubtaskCount}), defaulting to 3.`
);
finalSubtaskCount = 3;
}
// Determine prompt content AND system prompt
// Calculate the next subtask ID to match current behavior:
// - Start from the number of existing subtasks + 1
// - This creates sequential IDs: 1, 2, 3, 4...
// - Display format shows as parentTaskId.subtaskId (e.g., "1.1", "1.2", "2.1")
const nextSubtaskId = (task.subtasks?.length || 0) + 1;
// Load prompts using PromptManager
const promptManager = getPromptManager();
// Check if a codebase analysis provider is being used
const hasCodebaseAnalysisCapability = hasCodebaseAnalysis(
useResearch,
projectRoot,
session
);
// Combine all context sources into a single additionalContext parameter
let combinedAdditionalContext = '';
if (additionalContext || complexityReasoningContext) {
combinedAdditionalContext =
`\n\n${additionalContext}${complexityReasoningContext}`.trim();
}
if (gatheredContext) {
combinedAdditionalContext =
`${combinedAdditionalContext}\n\n# Project Context\n\n${gatheredContext}`.trim();
}
// Ensure expansionPrompt is a string (handle both string and object formats)
let expansionPromptText = undefined;
if (taskAnalysis?.expansionPrompt) {
if (typeof taskAnalysis.expansionPrompt === 'string') {
expansionPromptText = taskAnalysis.expansionPrompt;
} else if (
typeof taskAnalysis.expansionPrompt === 'object' &&
taskAnalysis.expansionPrompt.text
) {
expansionPromptText = taskAnalysis.expansionPrompt.text;
}
}
// Ensure gatheredContext is a string (handle both string and object formats)
let gatheredContextText = gatheredContext;
if (typeof gatheredContext === 'object' && gatheredContext !== null) {
if (gatheredContext.data) {
gatheredContextText = gatheredContext.data;
} else if (gatheredContext.text) {
gatheredContextText = gatheredContext.text;
} else {
gatheredContextText = JSON.stringify(gatheredContext);
}
}
const promptParams = {
task: task,
subtaskCount: finalSubtaskCount,
nextSubtaskId: nextSubtaskId,
additionalContext: additionalContext,
complexityReasoningContext: complexityReasoningContext,
gatheredContext: gatheredContextText || '',
useResearch: useResearch,
expansionPrompt: expansionPromptText || undefined,
hasCodebaseAnalysis: hasCodebaseAnalysisCapability,
projectRoot: projectRoot || ''
};
let variantKey = 'default';
if (expansionPromptText) {
variantKey = 'complexity-report';
logger.info(
`Using expansion prompt from complexity report for task ${task.id}.`
);
} else if (useResearch) {
variantKey = 'research';
logger.info(`Using research variant for task ${task.id}.`);
} else {
logger.info(`Using standard prompt generation for task ${task.id}.`);
}
const { systemPrompt, userPrompt: promptContent } =
promptManager.loadPrompt('expand-task', promptParams, variantKey);
// Debug logging to identify the issue
logger.debug(`Selected variant: ${variantKey}`);
logger.debug(
`Prompt params passed: ${JSON.stringify(promptParams, null, 2)}`
);
logger.debug(
`System prompt (first 500 chars): ${systemPrompt.substring(0, 500)}...`
);
logger.debug(
`User prompt (first 500 chars): ${promptContent.substring(0, 500)}...`
);
// --- End Complexity Report / Prompt Logic ---
// --- AI Subtask Generation using generateObjectService ---
let generatedSubtasks = [];
let loadingIndicator = null;
if (outputFormat === 'text') {
loadingIndicator = startLoadingIndicator(
`Generating ${finalSubtaskCount || 'appropriate number of'} subtasks...\n`
);
}
let aiServiceResponse = null;
try {
const role = useResearch ? 'research' : 'main';
// Call generateObjectService with the determined prompts and telemetry params
aiServiceResponse = await generateObjectService({
prompt: promptContent,
systemPrompt: systemPrompt,
role,
session,
projectRoot,
schema: COMMAND_SCHEMAS['expand-task'],
objectName: 'subtasks',
commandName: 'expand-task',
outputType: outputFormat
});
// With generateObject, we expect structured data – verify it before use
const mainResult = aiServiceResponse?.mainResult;
if (!mainResult || !Array.isArray(mainResult.subtasks)) {
throw new Error('AI response did not include a valid subtasks array.');
}
generatedSubtasks = mainResult.subtasks;
logger.info(`Received ${generatedSubtasks.length} subtasks from AI.`);
} catch (error) {
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
logger.error(
`Error during AI call or parsing for task ${taskId}: ${error.message}`, // Added task ID context
'error'
);
throw error;
} finally {
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
}
// --- Task Update & File Writing ---
// Ensure task.subtasks is an array before appending
if (!Array.isArray(task.subtasks)) {
task.subtasks = [];
}
// Append the newly generated and validated subtasks
task.subtasks.push(...generatedSubtasks);
// --- End Change: Append instead of replace ---
data.tasks[taskIndex] = task; // Assign the modified task back
writeJSON(tasksPath, data, projectRoot, tag);
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
// Display AI Usage Summary for CLI
if (
outputFormat === 'text' &&
aiServiceResponse &&
aiServiceResponse.telemetryData
) {
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
}
// Return the updated task object AND telemetry data
return {
task,
telemetryData: aiServiceResponse?.telemetryData,
tagInfo: aiServiceResponse?.tagInfo
};
} catch (error) {
// Catches errors from file reading, parsing, AI call etc.
logger.error(`Error expanding task ${taskId}: ${error.message}`, 'error');
if (outputFormat === 'text' && getDebugFlag(session)) {
console.error(error); // Log full stack in debug CLI mode
}
throw error; // Re-throw for the caller
}
}
export default expandTask;
```
--------------------------------------------------------------------------------
/apps/docs/best-practices/configuration-advanced.mdx:
--------------------------------------------------------------------------------
```markdown
---
title: Advanced Configuration
sidebarTitle: "Advanced Configuration"
---
Taskmaster uses two primary methods for configuration:
1. **`.taskmaster/config.json` File (Recommended - New Structure)**
- This JSON file stores most configuration settings, including AI model selections, parameters, logging levels, and project defaults.
- **Location:** This file is created in the `.taskmaster/` directory when you run the `task-master models --setup` interactive setup or initialize a new project with `task-master init`.
- **Migration:** Existing projects with `.taskmasterconfig` in the root will continue to work, but should be migrated to the new structure using `task-master migrate`.
- **Management:** Use the `task-master models --setup` command (or `models` MCP tool) to interactively create and manage this file. You can also set specific models directly using `task-master models --set-<role>=<model_id>`, adding `--ollama` or `--openrouter` flags for custom models. Manual editing is possible but not recommended unless you understand the structure.
- **Example Structure:**
```json
{
"models": {
"main": {
"provider": "anthropic",
"modelId": "claude-3-7-sonnet-20250219",
"maxTokens": 64000,
"temperature": 0.2,
"baseURL": "https://api.anthropic.com/v1"
},
"research": {
"provider": "perplexity",
"modelId": "sonar-pro",
"maxTokens": 8700,
"temperature": 0.1,
"baseURL": "https://api.perplexity.ai/v1"
},
"fallback": {
"provider": "anthropic",
"modelId": "claude-3-5-sonnet",
"maxTokens": 64000,
"temperature": 0.2
}
},
"global": {
"logLevel": "info",
"debug": false,
"defaultSubtasks": 5,
"defaultPriority": "medium",
"defaultTag": "master",
"projectName": "Your Project Name",
"ollamaBaseURL": "http://localhost:11434/api",
"azureBaseURL": "https://your-endpoint.azure.com/openai/deployments",
"vertexProjectId": "your-gcp-project-id",
"vertexLocation": "us-central1"
}
}
```
2. **Legacy `.taskmasterconfig` File (Backward Compatibility)**
- For projects that haven't migrated to the new structure yet.
- **Location:** Project root directory.
- **Migration:** Use `task-master migrate` to move this to `.taskmaster/config.json`.
- **Deprecation:** While still supported, you'll see warnings encouraging migration to the new structure.
## Environment Variables (`.env` file or MCP `env` block - For API Keys Only)
- Used **exclusively** for sensitive API keys and specific endpoint URLs.
- **Location:**
- For CLI usage: Create a `.env` file in your project root.
- For MCP/Cursor usage: Configure keys in the `env` section of your `.cursor/mcp.json` file.
- **Required API Keys (Depending on configured providers):**
- `ANTHROPIC_API_KEY`: Your Anthropic API key.
- `PERPLEXITY_API_KEY`: Your Perplexity API key.
- `OPENAI_API_KEY`: Your OpenAI API key.
- `GOOGLE_API_KEY`: Your Google API key (also used for Vertex AI provider).
- `MISTRAL_API_KEY`: Your Mistral API key.
- `AZURE_OPENAI_API_KEY`: Your Azure OpenAI API key (also requires `AZURE_OPENAI_ENDPOINT`).
- `OPENROUTER_API_KEY`: Your OpenRouter API key.
- `XAI_API_KEY`: Your X-AI API key.
- **Optional Endpoint Overrides:**
- **Per-role `baseURL` in `.taskmasterconfig`:** You can add a `baseURL` property to any model role (`main`, `research`, `fallback`) to override the default API endpoint for that provider. If omitted, the provider's standard endpoint is used.
- **Environment Variable Overrides (`<PROVIDER>_BASE_URL`):** For greater flexibility, especially with third-party services, you can set an environment variable like `OPENAI_BASE_URL` or `MISTRAL_BASE_URL`. This will override any `baseURL` set in the configuration file for that provider. This is the recommended way to connect to OpenAI-compatible APIs.
- `AZURE_OPENAI_ENDPOINT`: Required if using Azure OpenAI key (can also be set as `baseURL` for the Azure model role).
- `OLLAMA_BASE_URL`: Override the default Ollama API URL (Default: `http://localhost:11434/api`).
- `VERTEX_PROJECT_ID`: Your Google Cloud project ID for Vertex AI. Required when using the 'vertex' provider.
- `VERTEX_LOCATION`: Google Cloud region for Vertex AI (e.g., 'us-central1'). Default is 'us-central1'.
- `GOOGLE_APPLICATION_CREDENTIALS`: Path to service account credentials JSON file for Google Cloud auth (alternative to API key for Vertex AI).
- **Optional Auto-Update Control:**
- `TASKMASTER_SKIP_AUTO_UPDATE`: Set to '1' to disable automatic updates. Also automatically disabled in CI environments (when `CI` environment variable is set).
**Important:** Settings like model ID selections (`main`, `research`, `fallback`), `maxTokens`, `temperature`, `logLevel`, `defaultSubtasks`, `defaultPriority`, and `projectName` are **managed in `.taskmaster/config.json`** (or `.taskmasterconfig` for unmigrated projects), not environment variables.
## Tagged Task Lists Configuration (v0.17+)
Taskmaster includes a tagged task lists system for multi-context task management.
### Global Tag Settings
```json
"global": {
"defaultTag": "master"
}
```
- **`defaultTag`** (string): Default tag context for new operations (default: "master")
### Git Integration
Task Master provides manual git integration through the `--from-branch` option:
- **Manual Tag Creation**: Use `task-master add-tag --from-branch` to create a tag based on your current git branch name
- **User Control**: No automatic tag switching - you control when and how tags are created
- **Flexible Workflow**: Supports any git workflow without imposing rigid branch-tag mappings
## State Management File
Taskmaster uses `.taskmaster/state.json` to track tagged system runtime information:
```json
{
"currentTag": "master",
"lastSwitched": "2025-06-11T20:26:12.598Z",
"migrationNoticeShown": true
}
```
- **`currentTag`**: Currently active tag context
- **`lastSwitched`**: Timestamp of last tag switch
- **`migrationNoticeShown`**: Whether migration notice has been displayed
This file is automatically created during tagged system migration and should not be manually edited.
## Example `.env` File (for API Keys)
```
# Required API keys for providers configured in .taskmaster/config.json
ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
PERPLEXITY_API_KEY=pplx-your-key-here
# OPENAI_API_KEY=sk-your-key-here
# GOOGLE_API_KEY=AIzaSy...
# AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
# etc.
# Optional Endpoint Overrides
# Use a specific provider's base URL, e.g., for an OpenAI-compatible API
# OPENAI_BASE_URL=https://api.third-party.com/v1
#
# Azure OpenAI Configuration
# AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/ or https://your-endpoint-name.cognitiveservices.azure.com/openai/deployments
# OLLAMA_BASE_URL=http://custom-ollama-host:11434/api
# Google Vertex AI Configuration (Required if using 'vertex' provider)
# VERTEX_PROJECT_ID=your-gcp-project-id
```
## Troubleshooting
### Configuration Errors
- If Task Master reports errors about missing configuration or cannot find the config file, run `task-master models --setup` in your project root to create or repair the file.
- For new projects, config will be created at `.taskmaster/config.json`. For legacy projects, you may want to use `task-master migrate` to move to the new structure.
- Ensure API keys are correctly placed in your `.env` file (for CLI) or `.cursor/mcp.json` (for MCP) and are valid for the providers selected in your config file.
### If `task-master init` doesn't respond:
Try running it with Node directly:
```bash
node node_modules/claude-task-master/scripts/init.js
```
Or clone the repository and run:
```bash
git clone https://github.com/eyaltoledano/claude-task-master.git
cd claude-task-master
node scripts/init.js
```
## Provider-Specific Configuration
### Google Vertex AI Configuration
Google Vertex AI is Google Cloud's enterprise AI platform and requires specific configuration:
1. **Prerequisites**:
- A Google Cloud account with Vertex AI API enabled
- Either a Google API key with Vertex AI permissions OR a service account with appropriate roles
- A Google Cloud project ID
2. **Authentication Options**:
- **API Key**: Set the `GOOGLE_API_KEY` environment variable
- **Service Account**: Set `GOOGLE_APPLICATION_CREDENTIALS` to point to your service account JSON file
3. **Required Configuration**:
- Set `VERTEX_PROJECT_ID` to your Google Cloud project ID
- Set `VERTEX_LOCATION` to your preferred Google Cloud region (default: us-central1)
4. **Example Setup**:
```bash
# In .env file
GOOGLE_API_KEY=AIzaSyXXXXXXXXXXXXXXXXXXXXXXXXX
VERTEX_PROJECT_ID=my-gcp-project-123
VERTEX_LOCATION=us-central1
```
Or using service account:
```bash
# In .env file
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
VERTEX_PROJECT_ID=my-gcp-project-123
VERTEX_LOCATION=us-central1
```
5. **In .taskmaster/config.json**:
```json
"global": {
"vertexProjectId": "my-gcp-project-123",
"vertexLocation": "us-central1"
}
```
### Azure OpenAI Configuration
Azure OpenAI provides enterprise-grade OpenAI models through Microsoft's Azure cloud platform and requires specific configuration:
1. **Prerequisites**:
- An Azure account with an active subscription
- Azure OpenAI service resource created in the Azure portal
- Azure OpenAI API key and endpoint URL
- Deployed models (e.g., gpt-4o, gpt-4o-mini, gpt-4.1, etc) in your Azure OpenAI resource
2. **Authentication**:
- Set the `AZURE_OPENAI_API_KEY` environment variable with your Azure OpenAI API key
- Configure the endpoint URL using one of the methods below
3. **Configuration Options**:
**Option 1: Using Global Azure Base URL (affects all Azure models)**
```json
// In .taskmaster/config.json
{
"models": {
"main": {
"provider": "azure",
"modelId": "gpt-4o",
"maxTokens": 16000,
"temperature": 0.7
},
"fallback": {
"provider": "azure",
"modelId": "gpt-4o-mini",
"maxTokens": 10000,
"temperature": 0.7
}
},
"global": {
"azureBaseURL": "https://your-resource-name.azure.com/openai/deployments"
}
}
```
**Option 2: Using Per-Model Base URLs (recommended for flexibility)**
```json
// In .taskmaster/config.json
{
"models": {
"main": {
"provider": "azure",
"modelId": "gpt-4o",
"maxTokens": 16000,
"temperature": 0.7,
"baseURL": "https://your-resource-name.azure.com/openai/deployments"
},
"research": {
"provider": "perplexity",
"modelId": "sonar-pro",
"maxTokens": 8700,
"temperature": 0.1
},
"fallback": {
"provider": "azure",
"modelId": "gpt-4o-mini",
"maxTokens": 10000,
"temperature": 0.7,
"baseURL": "https://your-resource-name.azure.com/openai/deployments"
}
}
}
```
4. **Environment Variables**:
```bash
# In .env file
AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
# Optional: Override endpoint for all Azure models
AZURE_OPENAI_ENDPOINT=https://your-resource-name.azure.com/openai/deployments
```
5. **Important Notes**:
- **Model Deployment Names**: The `modelId` in your configuration should match the **deployment name** you created in Azure OpenAI Studio, not the underlying model name
- **Base URL Priority**: Per-model `baseURL` settings override the global `azureBaseURL` setting
- **Endpoint Format**: When using per-model `baseURL`, use the full path including `/openai/deployments`
6. **Troubleshooting**:
**"Resource not found" errors:**
- Ensure your `baseURL` includes the full path: `https://your-resource-name.openai.azure.com/openai/deployments`
- Verify that your deployment name in `modelId` exactly matches what's configured in Azure OpenAI Studio
- Check that your Azure OpenAI resource is in the correct region and properly deployed
**Authentication errors:**
- Verify your `AZURE_OPENAI_API_KEY` is correct and has not expired
- Ensure your Azure OpenAI resource has the necessary permissions
- Check that your subscription has not been suspended or reached quota limits
**Model availability errors:**
- Confirm the model is deployed in your Azure OpenAI resource
- Verify the deployment name matches your configuration exactly (case-sensitive)
- Ensure the model deployment is in a "Succeeded" state in Azure OpenAI Studio
- Ensure youre not getting rate limited by `maxTokens` maintain appropriate Tokens per Minute Rate Limit (TPM) in your deployment.
```
--------------------------------------------------------------------------------
/tests/unit/task-master.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* Tests for task-master.js initTaskMaster function
*/
import { jest } from '@jest/globals';
import path from 'path';
import fs from 'fs';
import os from 'os';
import { initTaskMaster, TaskMaster } from '../../src/task-master.js';
import {
TASKMASTER_DIR,
TASKMASTER_TASKS_FILE,
LEGACY_CONFIG_FILE,
TASKMASTER_CONFIG_FILE,
LEGACY_TASKS_FILE
} from '../../src/constants/paths.js';
// Mock the console to prevent noise during tests
jest.spyOn(console, 'error').mockImplementation(() => {});
describe('initTaskMaster', () => {
let tempDir;
let originalCwd;
beforeEach(() => {
// Create a temporary directory for testing
tempDir = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'taskmaster-test-'))
);
originalCwd = process.cwd();
// Clear all mocks
jest.clearAllMocks();
});
afterEach(() => {
// Restore original working directory
process.chdir(originalCwd);
// Clean up temporary directory
if (fs.existsSync(tempDir)) {
fs.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('Project root detection', () => {
test('should find project root when .taskmaster directory exists', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Change to temp directory
process.chdir(tempDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
expect(taskMaster).toBeInstanceOf(TaskMaster);
});
test('should find project root when legacy config file exists', () => {
// Arrange - Create legacy config file in temp dir
const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE);
fs.writeFileSync(legacyConfigPath, '{}');
// Change to temp directory
process.chdir(tempDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should find project root from subdirectory', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Create a subdirectory and change to it
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
process.chdir(srcDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should find project root from deeply nested subdirectory', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Create deeply nested subdirectory and change to it
const deepDir = path.join(tempDir, 'src', 'components', 'ui');
fs.mkdirSync(deepDir, { recursive: true });
process.chdir(deepDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should return cwd when no project markers found cuz we changed the behavior of this function', () => {
// Arrange - Empty temp directory, no project markers
process.chdir(tempDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
});
describe('Project root override validation', () => {
test('should accept valid project root override with .taskmaster directory', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Act
const taskMaster = initTaskMaster({ projectRoot: tempDir });
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should accept valid project root override with legacy config', () => {
// Arrange - Create legacy config file in temp dir
const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE);
fs.writeFileSync(legacyConfigPath, '{}');
// Act
const taskMaster = initTaskMaster({ projectRoot: tempDir });
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
test('should throw error when project root override does not exist', () => {
// Arrange - Non-existent path
const nonExistentPath = path.join(tempDir, 'does-not-exist');
// Act & Assert
expect(() => {
initTaskMaster({ projectRoot: nonExistentPath });
}).toThrow(
`Project root override path does not exist: ${nonExistentPath}`
);
});
test('should throw error when project root override has no project markers', () => {
// Arrange - Empty temp directory (no project markers)
// Act & Assert
expect(() => {
initTaskMaster({ projectRoot: tempDir });
}).toThrow(
`Project root override is not a valid taskmaster project: ${tempDir}`
);
});
test('should resolve relative project root override', () => {
// Arrange - Create .taskmaster directory in temp dir
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
// Create subdirectory and change to it
const srcDir = path.join(tempDir, 'src');
fs.mkdirSync(srcDir, { recursive: true });
process.chdir(srcDir);
// Act - Use relative path '../' to go back to project root
const taskMaster = initTaskMaster({ projectRoot: '../' });
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
});
});
describe('Path resolution with boolean logic', () => {
let taskMasterDir, tasksPath, configPath, statePath;
beforeEach(() => {
// Setup a valid project structure
taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
tasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE);
fs.mkdirSync(path.dirname(tasksPath), { recursive: true });
fs.writeFileSync(tasksPath, '[]');
configPath = path.join(tempDir, TASKMASTER_CONFIG_FILE);
fs.writeFileSync(configPath, '{}');
statePath = path.join(taskMasterDir, 'state.json');
fs.writeFileSync(statePath, '{}');
process.chdir(tempDir);
});
test('should return paths when required (true) and files exist', () => {
// Act
const taskMaster = initTaskMaster({
tasksPath: true,
configPath: true,
statePath: true
});
// Assert
expect(taskMaster.getTasksPath()).toBe(tasksPath);
expect(taskMaster.getConfigPath()).toBe(configPath);
expect(taskMaster.getStatePath()).toBe(statePath);
});
test('should throw error when required (true) files do not exist', () => {
// Arrange - Remove tasks file
fs.unlinkSync(tasksPath);
// Act & Assert
expect(() => {
initTaskMaster({ tasksPath: true });
}).toThrow(
'Required tasks file not found. Searched: .taskmaster/tasks/tasks.json, tasks/tasks.json'
);
});
test('should return null when optional (false/undefined) files do not exist', () => {
// Arrange - Remove tasks file
fs.unlinkSync(tasksPath);
// Act
const taskMaster = initTaskMaster({
tasksPath: false
});
// Assert
expect(taskMaster.getTasksPath()).toBeNull();
});
test('should return default paths when optional files not specified in overrides', () => {
// Arrange - Remove all optional files
fs.unlinkSync(tasksPath);
fs.unlinkSync(configPath);
fs.unlinkSync(statePath);
// Act - Don't specify any optional paths
const taskMaster = initTaskMaster({});
// Assert - Should return absolute paths with default locations
expect(taskMaster.getTasksPath()).toBe(
path.join(tempDir, TASKMASTER_TASKS_FILE)
);
expect(taskMaster.getConfigPath()).toBe(
path.join(tempDir, TASKMASTER_CONFIG_FILE)
);
expect(taskMaster.getStatePath()).toBe(
path.join(tempDir, TASKMASTER_DIR, 'state.json')
);
});
});
describe('String path overrides', () => {
let taskMasterDir;
beforeEach(() => {
taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
process.chdir(tempDir);
});
test('should accept valid absolute path override', () => {
// Arrange - Create custom tasks file
const customTasksPath = path.join(tempDir, 'custom-tasks.json');
fs.writeFileSync(customTasksPath, '[]');
// Act
const taskMaster = initTaskMaster({
tasksPath: customTasksPath
});
// Assert
expect(taskMaster.getTasksPath()).toBe(customTasksPath);
});
test('should accept valid relative path override', () => {
// Arrange - Create custom tasks file
const customTasksPath = path.join(tempDir, 'custom-tasks.json');
fs.writeFileSync(customTasksPath, '[]');
// Act
const taskMaster = initTaskMaster({
tasksPath: './custom-tasks.json'
});
// Assert
expect(taskMaster.getTasksPath()).toBe(customTasksPath);
});
test('should throw error when string path override does not exist', () => {
// Arrange - Non-existent file path
const nonExistentPath = path.join(tempDir, 'does-not-exist.json');
// Act & Assert
expect(() => {
initTaskMaster({ tasksPath: nonExistentPath });
}).toThrow(`tasks file override path does not exist: ${nonExistentPath}`);
});
});
describe('Legacy file support', () => {
beforeEach(() => {
// Setup basic project structure
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
process.chdir(tempDir);
});
test('should find legacy tasks file when new format does not exist', () => {
// Arrange - Create legacy tasks file
const legacyTasksDir = path.join(tempDir, 'tasks');
fs.mkdirSync(legacyTasksDir, { recursive: true });
const legacyTasksPath = path.join(tempDir, LEGACY_TASKS_FILE);
fs.writeFileSync(legacyTasksPath, '[]');
// Act
const taskMaster = initTaskMaster({ tasksPath: true });
// Assert
expect(taskMaster.getTasksPath()).toBe(legacyTasksPath);
});
test('should prefer new format over legacy when both exist', () => {
// Arrange - Create both new and legacy files
const newTasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE);
fs.mkdirSync(path.dirname(newTasksPath), { recursive: true });
fs.writeFileSync(newTasksPath, '[]');
const legacyTasksDir = path.join(tempDir, 'tasks');
fs.mkdirSync(legacyTasksDir, { recursive: true });
const legacyTasksPath = path.join(tempDir, LEGACY_TASKS_FILE);
fs.writeFileSync(legacyTasksPath, '[]');
// Act
const taskMaster = initTaskMaster({ tasksPath: true });
// Assert
expect(taskMaster.getTasksPath()).toBe(newTasksPath);
});
test('should find legacy config file when new format does not exist', () => {
// Arrange - Create legacy config file
const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE);
fs.writeFileSync(legacyConfigPath, '{}');
// Act
const taskMaster = initTaskMaster({ configPath: true });
// Assert
expect(taskMaster.getConfigPath()).toBe(legacyConfigPath);
});
});
describe('TaskMaster class methods', () => {
test('should return all paths via getAllPaths method', () => {
// Arrange - Setup project with all files
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
const tasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE);
fs.mkdirSync(path.dirname(tasksPath), { recursive: true });
fs.writeFileSync(tasksPath, '[]');
const configPath = path.join(tempDir, TASKMASTER_CONFIG_FILE);
fs.writeFileSync(configPath, '{}');
process.chdir(tempDir);
// Act
const taskMaster = initTaskMaster({
tasksPath: true,
configPath: true
});
// Assert
const allPaths = taskMaster.getAllPaths();
expect(allPaths).toEqual(
expect.objectContaining({
projectRoot: tempDir,
taskMasterDir: taskMasterDir,
tasksPath: tasksPath,
configPath: configPath
})
);
// Verify paths object is frozen
expect(() => {
allPaths.projectRoot = '/different/path';
}).toThrow();
});
test('should return correct individual paths', () => {
// Arrange
const taskMasterDir = path.join(tempDir, TASKMASTER_DIR);
fs.mkdirSync(taskMasterDir, { recursive: true });
process.chdir(tempDir);
// Act
const taskMaster = initTaskMaster({});
// Assert
expect(taskMaster.getProjectRoot()).toBe(tempDir);
expect(taskMaster.getTaskMasterDir()).toBe(taskMasterDir);
// Default paths are always set for tasks, config, and state
expect(taskMaster.getTasksPath()).toBe(
path.join(tempDir, TASKMASTER_TASKS_FILE)
);
expect(taskMaster.getConfigPath()).toBe(
path.join(tempDir, TASKMASTER_CONFIG_FILE)
);
expect(taskMaster.getStatePath()).toBe(
path.join(taskMasterDir, 'state.json')
);
// PRD and complexity report paths are undefined when not provided
expect(typeof taskMaster.getComplexityReportPath()).toBe('string');
expect(taskMaster.getComplexityReportPath()).toMatch(
/task-complexity-report\.json$/
);
});
});
});
```
--------------------------------------------------------------------------------
/src/ui/parse-prd.js:
--------------------------------------------------------------------------------
```javascript
/**
* parse-prd.js
* UI functions specifically for PRD parsing operations
*/
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { formatElapsedTime } from '../utils/format.js';
// Constants
const CONSTANTS = {
BAR_WIDTH: 40,
TABLE_COL_WIDTHS: [28, 50],
DEFAULT_MODEL: 'Default',
DEFAULT_TEMPERATURE: 0.7
};
const PRIORITIES = {
HIGH: 'high',
MEDIUM: 'medium',
LOW: 'low'
};
const PRIORITY_COLORS = {
[PRIORITIES.HIGH]: '#CC0000',
[PRIORITIES.MEDIUM]: '#FF8800',
[PRIORITIES.LOW]: '#FFCC00'
};
// Reusable box styles
const BOX_STYLES = {
main: {
padding: { top: 1, bottom: 1, left: 2, right: 2 },
margin: { top: 0, bottom: 0 },
borderColor: 'blue',
borderStyle: 'round'
},
summary: {
padding: { top: 1, right: 1, bottom: 1, left: 1 },
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, right: 1, bottom: 1, left: 0 }
},
warning: {
padding: 1,
borderColor: 'yellow',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
},
nextSteps: {
padding: 1,
borderColor: 'cyan',
borderStyle: 'round',
margin: { top: 1, right: 0, bottom: 1, left: 0 }
}
};
/**
* Helper function for building main message content
* @param {Object} params - Message parameters
* @param {string} params.prdFilePath - Path to the PRD file
* @param {string} params.outputPath - Path where tasks will be saved
* @param {number} params.numTasks - Number of tasks to generate
* @param {string} params.model - AI model name
* @param {number} params.temperature - AI temperature setting
* @param {boolean} params.append - Whether appending to existing tasks
* @param {boolean} params.research - Whether research mode is enabled
* @returns {string} The formatted message content
*/
function buildMainMessage({
prdFilePath,
outputPath,
numTasks,
model,
temperature,
append,
research
}) {
const actionVerb = append ? 'Appending' : 'Generating';
let modelLine = `Model: ${model} | Temperature: ${temperature}`;
if (research) {
modelLine += ` | ${chalk.cyan.bold('🔬 Research Mode')}`;
}
return (
chalk.bold(`🤖 Parsing PRD and ${actionVerb} Tasks`) +
'\n' +
chalk.dim(modelLine) +
'\n\n' +
chalk.blue(`Input: ${prdFilePath}`) +
'\n' +
chalk.blue(`Output: ${outputPath}`) +
'\n' +
chalk.blue(`Tasks to ${append ? 'Append' : 'Generate'}: ${numTasks}`)
);
}
/**
* Helper function for displaying the main message box
* @param {string} message - The message content to display in the box
*/
function displayMainMessageBox(message) {
console.log(boxen(message, BOX_STYLES.main));
}
/**
* Helper function for displaying append mode notice
* @param {number} existingTasksCount - Number of existing tasks
* @param {number} nextId - Next ID to be used
*/
function displayAppendModeNotice(existingTasksCount, nextId) {
console.log(
chalk.yellow.bold('📝 Append mode') +
` - Adding to ${existingTasksCount} existing tasks (next ID: ${nextId})`
);
}
/**
* Helper function for force mode messages
* @param {boolean} append - Whether in append mode
* @returns {string} The formatted force mode message
*/
function createForceMessage(append) {
const baseMessage = chalk.red.bold('⚠️ Force flag enabled');
return append
? `${baseMessage} - Will overwrite if conflicts occur`
: `${baseMessage} - Overwriting existing tasks`;
}
/**
* Display the start of PRD parsing with a boxen announcement
* @param {Object} options - Options for PRD parsing start
* @param {string} options.prdFilePath - Path to the PRD file being parsed
* @param {string} options.outputPath - Path where the tasks will be saved
* @param {number} options.numTasks - Number of tasks to generate
* @param {string} [options.model] - AI model name
* @param {number} [options.temperature] - AI temperature setting
* @param {boolean} [options.append=false] - Whether to append to existing tasks
* @param {boolean} [options.research=false] - Whether research mode is enabled
* @param {boolean} [options.force=false] - Whether force mode is enabled
* @param {Array} [options.existingTasks=[]] - Existing tasks array
* @param {number} [options.nextId=1] - Next ID to be used
*/
function displayParsePrdStart({
prdFilePath,
outputPath,
numTasks,
model = CONSTANTS.DEFAULT_MODEL,
temperature = CONSTANTS.DEFAULT_TEMPERATURE,
append = false,
research = false,
force = false,
existingTasks = [],
nextId = 1
}) {
// Input validation
if (
!prdFilePath ||
typeof prdFilePath !== 'string' ||
prdFilePath.trim() === ''
) {
throw new Error('prdFilePath is required and must be a non-empty string');
}
if (
!outputPath ||
typeof outputPath !== 'string' ||
outputPath.trim() === ''
) {
throw new Error('outputPath is required and must be a non-empty string');
}
// Build and display the main message box
const message = buildMainMessage({
prdFilePath,
outputPath,
numTasks,
model,
temperature,
append,
research
});
displayMainMessageBox(message);
// Display append/force notices beneath the boxen if either flag is set
if (append || force) {
// Add append mode details if enabled
if (append) {
displayAppendModeNotice(existingTasks.length, nextId);
}
// Add force mode details if enabled
if (force) {
console.log(createForceMessage(append));
}
// Add a blank line after notices for spacing
console.log();
}
}
/**
* Calculate priority statistics
* @param {Object} taskPriorities - Priority counts object
* @param {number} totalTasks - Total number of tasks
* @returns {Object} Priority statistics with counts and percentages
*/
function calculatePriorityStats(taskPriorities, totalTasks) {
const stats = {};
Object.values(PRIORITIES).forEach((priority) => {
const count = taskPriorities[priority] || 0;
stats[priority] = {
count,
percentage: totalTasks > 0 ? Math.round((count / totalTasks) * 100) : 0
};
});
return stats;
}
/**
* Calculate bar character distribution for priorities
* @param {Object} priorityStats - Priority statistics
* @param {number} totalTasks - Total number of tasks
* @returns {Object} Character counts for each priority
*/
function calculateBarDistribution(priorityStats, totalTasks) {
const barWidth = CONSTANTS.BAR_WIDTH;
const distribution = {};
if (totalTasks === 0) {
Object.values(PRIORITIES).forEach((priority) => {
distribution[priority] = 0;
});
return distribution;
}
// Calculate raw proportions
const rawChars = {};
Object.values(PRIORITIES).forEach((priority) => {
rawChars[priority] =
(priorityStats[priority].count / totalTasks) * barWidth;
});
// Initial distribution - floor values
Object.values(PRIORITIES).forEach((priority) => {
distribution[priority] = Math.floor(rawChars[priority]);
});
// Ensure non-zero priorities get at least 1 character
Object.values(PRIORITIES).forEach((priority) => {
if (priorityStats[priority].count > 0 && distribution[priority] === 0) {
distribution[priority] = 1;
}
});
// Distribute remaining characters based on decimal parts
const currentTotal = Object.values(distribution).reduce(
(sum, val) => sum + val,
0
);
const remainingChars = barWidth - currentTotal;
if (remainingChars > 0) {
const decimals = Object.values(PRIORITIES)
.map((priority) => ({
priority,
decimal: rawChars[priority] - Math.floor(rawChars[priority])
}))
.sort((a, b) => b.decimal - a.decimal);
for (let i = 0; i < remainingChars && i < decimals.length; i++) {
distribution[decimals[i].priority]++;
}
}
return distribution;
}
/**
* Create priority distribution bar visual
* @param {Object} barDistribution - Character distribution for priorities
* @returns {string} Visual bar string
*/
function createPriorityBar(barDistribution) {
let bar = '';
bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])(
'█'.repeat(barDistribution[PRIORITIES.HIGH])
);
bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])(
'█'.repeat(barDistribution[PRIORITIES.MEDIUM])
);
bar += chalk.yellow('█'.repeat(barDistribution[PRIORITIES.LOW]));
const totalChars = Object.values(barDistribution).reduce(
(sum, val) => sum + val,
0
);
if (totalChars < CONSTANTS.BAR_WIDTH) {
bar += chalk.gray('░'.repeat(CONSTANTS.BAR_WIDTH - totalChars));
}
return bar;
}
/**
* Build priority distribution row for table
* @param {Object} priorityStats - Priority statistics
* @returns {Array} Table row for priority distribution
*/
function buildPriorityRow(priorityStats) {
const parts = [];
Object.entries(PRIORITIES).forEach(([key, priority]) => {
const stats = priorityStats[priority];
const color =
priority === PRIORITIES.HIGH
? chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])
: priority === PRIORITIES.MEDIUM
? chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])
: chalk.yellow;
const label = key.charAt(0) + key.slice(1).toLowerCase();
parts.push(
`${color.bold(stats.count)} ${color(label)} (${stats.percentage}%)`
);
});
return [chalk.cyan('Priority distribution:'), parts.join(' · ')];
}
/**
* Display a summary of the PRD parsing results
* @param {Object} summary - Summary of the parsing results
* @param {number} summary.totalTasks - Total number of tasks generated
* @param {string} summary.prdFilePath - Path to the PRD file
* @param {string} summary.outputPath - Path where the tasks were saved
* @param {number} summary.elapsedTime - Total elapsed time in seconds
* @param {Object} summary.taskPriorities - Breakdown of tasks by category/priority
* @param {boolean} summary.usedFallback - Whether fallback parsing was used
* @param {string} summary.actionVerb - Whether tasks were 'generated' or 'appended'
*/
function displayParsePrdSummary(summary) {
const {
totalTasks,
taskPriorities = {},
prdFilePath,
outputPath,
elapsedTime,
usedFallback = false,
actionVerb = 'generated'
} = summary;
// Format the elapsed time
const timeDisplay = formatElapsedTime(elapsedTime);
// Create a table for better alignment
const table = new Table({
chars: {
top: '',
'top-mid': '',
'top-left': '',
'top-right': '',
bottom: '',
'bottom-mid': '',
'bottom-left': '',
'bottom-right': '',
left: '',
'left-mid': '',
mid: '',
'mid-mid': '',
right: '',
'right-mid': '',
middle: ' '
},
style: { border: [], 'padding-left': 2 },
colWidths: CONSTANTS.TABLE_COL_WIDTHS
});
// Basic info
// Use the action verb to properly display if tasks were generated or appended
table.push(
[chalk.cyan(`Total tasks ${actionVerb}:`), chalk.bold(totalTasks)],
[chalk.cyan('Processing time:'), chalk.bold(timeDisplay)]
);
// Priority distribution if available
if (taskPriorities && Object.keys(taskPriorities).length > 0) {
const priorityStats = calculatePriorityStats(taskPriorities, totalTasks);
const priorityRow = buildPriorityRow(priorityStats);
table.push(priorityRow);
// Visual bar representation
const barDistribution = calculateBarDistribution(priorityStats, totalTasks);
const distributionBar = createPriorityBar(barDistribution);
table.push([chalk.cyan('Distribution:'), distributionBar]);
}
// Add file paths
table.push(
[chalk.cyan('PRD source:'), chalk.italic(prdFilePath)],
[chalk.cyan('Tasks file:'), chalk.italic(outputPath)]
);
// Add fallback parsing indicator if applicable
if (usedFallback) {
table.push([
chalk.yellow('Fallback parsing:'),
chalk.yellow('✓ Used fallback parsing')
]);
}
// Final string output with title and footer
const output = [
chalk.bold.underline(
`PRD Parsing Complete - Tasks ${actionVerb.charAt(0).toUpperCase() + actionVerb.slice(1)}`
),
'',
table.toString()
].join('\n');
// Display the summary box
console.log(boxen(output, BOX_STYLES.summary));
// Show fallback parsing warning if needed
if (usedFallback) {
displayFallbackWarning();
}
// Show next steps
displayNextSteps();
}
/**
* Display fallback parsing warning
*/
function displayFallbackWarning() {
const warningContent =
chalk.yellow.bold('⚠️ Fallback Parsing Used') +
'\n\n' +
chalk.white(
'The system used fallback parsing to complete task generation.'
) +
'\n' +
chalk.white(
'This typically happens when streaming JSON parsing is incomplete.'
) +
'\n' +
chalk.white('Your tasks were successfully generated, but consider:') +
'\n' +
chalk.white('• Reviewing task completeness') +
'\n' +
chalk.white('• Checking for any missing details') +
'\n\n' +
chalk.white("This is normal and usually doesn't indicate any issues.");
console.log(boxen(warningContent, BOX_STYLES.warning));
}
/**
* Display next steps after parsing
*/
function displayNextSteps() {
const stepsContent =
chalk.white.bold('Next Steps:') +
'\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` +
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master analyze-complexity')} to analyze task complexity`;
console.log(boxen(stepsContent, BOX_STYLES.nextSteps));
}
export { displayParsePrdStart, displayParsePrdSummary, formatElapsedTime };
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/commands/move-cross-tag.test.js:
--------------------------------------------------------------------------------
```javascript
import { jest } from '@jest/globals';
import chalk from 'chalk';
// ============================================================================
// MOCK FACTORY & CONFIGURATION SYSTEM
// ============================================================================
/**
* Mock configuration object to enable/disable specific mocks per test
*/
const mockConfig = {
// Core functionality mocks (always needed)
core: {
moveTasksBetweenTags: true,
generateTaskFiles: true,
readJSON: true,
initTaskMaster: true,
findProjectRoot: true
},
// Console and process mocks
console: {
error: true,
log: true,
exit: true
},
// TaskMaster instance mocks
taskMaster: {
getCurrentTag: true,
getTasksPath: true,
getProjectRoot: true
}
};
/**
* Creates mock functions with consistent naming
*/
function createMock(name) {
return jest.fn().mockName(name);
}
/**
* Mock factory for creating focused mocks based on configuration
*/
function createMockFactory(config = mockConfig) {
const mocks = {};
// Core functionality mocks
if (config.core?.moveTasksBetweenTags) {
mocks.moveTasksBetweenTags = createMock('moveTasksBetweenTags');
}
if (config.core?.generateTaskFiles) {
mocks.generateTaskFiles = createMock('generateTaskFiles');
}
if (config.core?.readJSON) {
mocks.readJSON = createMock('readJSON');
}
if (config.core?.initTaskMaster) {
mocks.initTaskMaster = createMock('initTaskMaster');
}
if (config.core?.findProjectRoot) {
mocks.findProjectRoot = createMock('findProjectRoot');
}
return mocks;
}
/**
* Sets up mocks based on configuration
*/
function setupMocks(config = mockConfig) {
const mocks = createMockFactory(config);
// Only mock the modules that are actually used in cross-tag move functionality
if (config.core?.moveTasksBetweenTags) {
jest.mock(
'../../../../../scripts/modules/task-manager/move-task.js',
() => ({
moveTasksBetweenTags: mocks.moveTasksBetweenTags
})
);
}
if (
config.core?.generateTaskFiles ||
config.core?.readJSON ||
config.core?.findProjectRoot
) {
jest.mock('../../../../../scripts/modules/utils.js', () => ({
findProjectRoot: mocks.findProjectRoot,
generateTaskFiles: mocks.generateTaskFiles,
readJSON: mocks.readJSON,
// Minimal set of utils that might be used
log: jest.fn(),
writeJSON: jest.fn(),
getCurrentTag: jest.fn(() => 'master')
}));
}
if (config.core?.initTaskMaster) {
jest.mock('../../../../../scripts/modules/config-manager.js', () => ({
initTaskMaster: mocks.initTaskMaster,
isApiKeySet: jest.fn(() => true),
getConfig: jest.fn(() => ({}))
}));
}
// Mock chalk for consistent output testing
jest.mock('chalk', () => ({
red: jest.fn((text) => text),
blue: jest.fn((text) => text),
green: jest.fn((text) => text),
yellow: jest.fn((text) => text),
white: jest.fn((text) => ({
bold: jest.fn((text) => text)
})),
reset: jest.fn((text) => text)
}));
return mocks;
}
// ============================================================================
// TEST SETUP
// ============================================================================
// Set up mocks with default configuration
const mocks = setupMocks();
// Import the actual command handler functions
import { registerCommands } from '../../../../../scripts/modules/commands.js';
// Extract the handleCrossTagMove function from the commands module
// This is a simplified version of the actual function for testing
async function handleCrossTagMove(moveContext, options) {
const { sourceId, sourceTag, toTag, taskMaster } = moveContext;
if (!sourceId) {
console.error('Error: --from parameter is required for cross-tag moves');
process.exit(1);
throw new Error('--from parameter is required for cross-tag moves');
}
if (sourceTag === toTag) {
console.error(
`Error: Source and target tags are the same ("${sourceTag}")`
);
process.exit(1);
throw new Error(`Source and target tags are the same ("${sourceTag}")`);
}
const sourceIds = sourceId.split(',').map((id) => id.trim());
const moveOptions = {
withDependencies: options.withDependencies || false,
ignoreDependencies: options.ignoreDependencies || false
};
const result = await mocks.moveTasksBetweenTags(
taskMaster.getTasksPath(),
sourceIds,
sourceTag,
toTag,
moveOptions,
{ projectRoot: taskMaster.getProjectRoot() }
);
// Check if source tag still contains tasks before regenerating files
const tasksData = mocks.readJSON(
taskMaster.getTasksPath(),
taskMaster.getProjectRoot(),
sourceTag
);
const sourceTagHasTasks =
tasksData && Array.isArray(tasksData.tasks) && tasksData.tasks.length > 0;
// Generate task files for the affected tags
await mocks.generateTaskFiles(taskMaster.getTasksPath(), 'tasks', {
tag: toTag,
projectRoot: taskMaster.getProjectRoot()
});
// Only regenerate source tag files if it still contains tasks
if (sourceTagHasTasks) {
await mocks.generateTaskFiles(taskMaster.getTasksPath(), 'tasks', {
tag: sourceTag,
projectRoot: taskMaster.getProjectRoot()
});
}
return result;
}
// ============================================================================
// TEST SUITE
// ============================================================================
describe('CLI Move Command Cross-Tag Functionality', () => {
let mockTaskMaster;
let mockConsoleError;
let mockConsoleLog;
let mockProcessExit;
beforeEach(() => {
jest.clearAllMocks();
// Mock console methods
mockConsoleError = jest.spyOn(console, 'error').mockImplementation();
mockConsoleLog = jest.spyOn(console, 'log').mockImplementation();
mockProcessExit = jest.spyOn(process, 'exit').mockImplementation();
// Mock TaskMaster instance
mockTaskMaster = {
getCurrentTag: jest.fn().mockReturnValue('master'),
getTasksPath: jest.fn().mockReturnValue('/test/path/tasks.json'),
getProjectRoot: jest.fn().mockReturnValue('/test/project')
};
mocks.initTaskMaster.mockReturnValue(mockTaskMaster);
mocks.findProjectRoot.mockReturnValue('/test/project');
mocks.generateTaskFiles.mockResolvedValue();
mocks.readJSON.mockReturnValue({
tasks: [
{ id: 1, title: 'Test Task 1' },
{ id: 2, title: 'Test Task 2' }
]
});
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('Cross-Tag Move Logic', () => {
it('should handle basic cross-tag move', async () => {
const options = {
from: '1',
fromTag: 'backlog',
toTag: 'in-progress',
withDependencies: false,
ignoreDependencies: false
};
const moveContext = {
sourceId: options.from,
sourceTag: options.fromTag,
toTag: options.toTag,
taskMaster: mockTaskMaster
};
mocks.moveTasksBetweenTags.mockResolvedValue({
message: 'Successfully moved 1 tasks from "backlog" to "in-progress"'
});
await handleCrossTagMove(moveContext, options);
expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
'/test/path/tasks.json',
['1'],
'backlog',
'in-progress',
{
withDependencies: false,
ignoreDependencies: false
},
{ projectRoot: '/test/project' }
);
});
it('should handle --with-dependencies flag', async () => {
const options = {
from: '1',
fromTag: 'backlog',
toTag: 'in-progress',
withDependencies: true,
ignoreDependencies: false
};
const moveContext = {
sourceId: options.from,
sourceTag: options.fromTag,
toTag: options.toTag,
taskMaster: mockTaskMaster
};
mocks.moveTasksBetweenTags.mockResolvedValue({
message: 'Successfully moved 2 tasks from "backlog" to "in-progress"'
});
await handleCrossTagMove(moveContext, options);
expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
'/test/path/tasks.json',
['1'],
'backlog',
'in-progress',
{
withDependencies: true,
ignoreDependencies: false
},
{ projectRoot: '/test/project' }
);
});
it('should handle --ignore-dependencies flag', async () => {
const options = {
from: '1',
fromTag: 'backlog',
toTag: 'in-progress',
withDependencies: false,
ignoreDependencies: true
};
const moveContext = {
sourceId: options.from,
sourceTag: options.fromTag,
toTag: options.toTag,
taskMaster: mockTaskMaster
};
mocks.moveTasksBetweenTags.mockResolvedValue({
message: 'Successfully moved 1 tasks from "backlog" to "in-progress"'
});
await handleCrossTagMove(moveContext, options);
expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
'/test/path/tasks.json',
['1'],
'backlog',
'in-progress',
{
withDependencies: false,
ignoreDependencies: true
},
{ projectRoot: '/test/project' }
);
});
});
describe('Error Handling', () => {
it('should handle missing --from parameter', async () => {
const options = {
from: undefined,
fromTag: 'backlog',
toTag: 'in-progress'
};
const moveContext = {
sourceId: options.from,
sourceTag: options.fromTag,
toTag: options.toTag,
taskMaster: mockTaskMaster
};
await expect(handleCrossTagMove(moveContext, options)).rejects.toThrow();
expect(mockConsoleError).toHaveBeenCalledWith(
'Error: --from parameter is required for cross-tag moves'
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
it('should handle same source and target tags', async () => {
const options = {
from: '1',
fromTag: 'backlog',
toTag: 'backlog'
};
const moveContext = {
sourceId: options.from,
sourceTag: options.fromTag,
toTag: options.toTag,
taskMaster: mockTaskMaster
};
await expect(handleCrossTagMove(moveContext, options)).rejects.toThrow();
expect(mockConsoleError).toHaveBeenCalledWith(
'Error: Source and target tags are the same ("backlog")'
);
expect(mockProcessExit).toHaveBeenCalledWith(1);
});
});
describe('Fallback to Current Tag', () => {
it('should use current tag when --from-tag is not provided', async () => {
const options = {
from: '1',
fromTag: undefined,
toTag: 'in-progress'
};
const moveContext = {
sourceId: options.from,
sourceTag: 'master', // Should use current tag
toTag: options.toTag,
taskMaster: mockTaskMaster
};
mocks.moveTasksBetweenTags.mockResolvedValue({
message: 'Successfully moved 1 tasks from "master" to "in-progress"'
});
await handleCrossTagMove(moveContext, options);
expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
'/test/path/tasks.json',
['1'],
'master',
'in-progress',
expect.any(Object),
{ projectRoot: '/test/project' }
);
});
});
describe('Multiple Task Movement', () => {
it('should handle comma-separated task IDs', async () => {
const options = {
from: '1,2,3',
fromTag: 'backlog',
toTag: 'in-progress'
};
const moveContext = {
sourceId: options.from,
sourceTag: options.fromTag,
toTag: options.toTag,
taskMaster: mockTaskMaster
};
mocks.moveTasksBetweenTags.mockResolvedValue({
message: 'Successfully moved 3 tasks from "backlog" to "in-progress"'
});
await handleCrossTagMove(moveContext, options);
expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
'/test/path/tasks.json',
['1', '2', '3'],
'backlog',
'in-progress',
expect.any(Object),
{ projectRoot: '/test/project' }
);
});
it('should handle whitespace in comma-separated task IDs', async () => {
const options = {
from: '1, 2, 3',
fromTag: 'backlog',
toTag: 'in-progress'
};
const moveContext = {
sourceId: options.from,
sourceTag: options.fromTag,
toTag: options.toTag,
taskMaster: mockTaskMaster
};
mocks.moveTasksBetweenTags.mockResolvedValue({
message: 'Successfully moved 3 tasks from "backlog" to "in-progress"'
});
await handleCrossTagMove(moveContext, options);
expect(mocks.moveTasksBetweenTags).toHaveBeenCalledWith(
'/test/path/tasks.json',
['1', '2', '3'],
'backlog',
'in-progress',
expect.any(Object),
{ projectRoot: '/test/project' }
);
});
});
describe('Mock Configuration Tests', () => {
it('should work with minimal mock configuration', async () => {
// Test that the mock factory works with minimal config
const minimalConfig = {
core: {
moveTasksBetweenTags: true,
generateTaskFiles: true,
readJSON: true
}
};
const minimalMocks = createMockFactory(minimalConfig);
expect(minimalMocks.moveTasksBetweenTags).toBeDefined();
expect(minimalMocks.generateTaskFiles).toBeDefined();
expect(minimalMocks.readJSON).toBeDefined();
});
it('should allow disabling specific mocks', async () => {
// Test that mocks can be selectively disabled
const selectiveConfig = {
core: {
moveTasksBetweenTags: true,
generateTaskFiles: false, // Disabled
readJSON: true
}
};
const selectiveMocks = createMockFactory(selectiveConfig);
expect(selectiveMocks.moveTasksBetweenTags).toBeDefined();
expect(selectiveMocks.generateTaskFiles).toBeUndefined();
expect(selectiveMocks.readJSON).toBeDefined();
});
});
});
```
--------------------------------------------------------------------------------
/tests/unit/mcp/tools/remove-task.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* Tests for the remove-task MCP tool
*
* Note: This test does NOT test the actual implementation. It tests that:
* 1. The tool is registered correctly with the correct parameters
* 2. Arguments are passed correctly to removeTaskDirect
* 3. Error handling works as expected
* 4. Tag parameter is properly handled and passed through
*
* We do NOT import the real implementation - everything is mocked
*/
import { jest } from '@jest/globals';
// Mock EVERYTHING
const mockRemoveTaskDirect = jest.fn();
jest.mock('../../../../mcp-server/src/core/task-master-core.js', () => ({
removeTaskDirect: mockRemoveTaskDirect
}));
const mockHandleApiResult = jest.fn((result) => result);
const mockWithNormalizedProjectRoot = jest.fn((fn) => fn);
const mockCreateErrorResponse = jest.fn((msg) => ({
success: false,
error: { code: 'ERROR', message: msg }
}));
const mockFindTasksPath = jest.fn(() => '/mock/project/tasks.json');
jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
handleApiResult: mockHandleApiResult,
createErrorResponse: mockCreateErrorResponse,
withNormalizedProjectRoot: mockWithNormalizedProjectRoot
}));
jest.mock('../../../../mcp-server/src/core/utils/path-utils.js', () => ({
findTasksPath: mockFindTasksPath
}));
// Mock the z object from zod
const mockZod = {
object: jest.fn(() => mockZod),
string: jest.fn(() => mockZod),
boolean: jest.fn(() => mockZod),
optional: jest.fn(() => mockZod),
describe: jest.fn(() => mockZod),
_def: {
shape: () => ({
id: {},
file: {},
projectRoot: {},
confirm: {},
tag: {}
})
}
};
jest.mock('zod', () => ({
z: mockZod
}));
// DO NOT import the real module - create a fake implementation
// This is the fake implementation of registerRemoveTaskTool
const registerRemoveTaskTool = (server) => {
// Create simplified version of the tool config
const toolConfig = {
name: 'remove_task',
description: 'Remove a task or subtask permanently from the tasks list',
parameters: mockZod,
// Create a simplified mock of the execute function
execute: mockWithNormalizedProjectRoot(async (args, context) => {
const { log, session } = context;
try {
log.info && log.info(`Removing task(s) with ID(s): ${args.id}`);
// Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot)
let tasksJsonPath;
try {
tasksJsonPath = mockFindTasksPath(
{ projectRoot: args.projectRoot, file: args.file },
log
);
} catch (error) {
log.error && log.error(`Error finding tasks.json: ${error.message}`);
return mockCreateErrorResponse(
`Failed to find tasks.json: ${error.message}`
);
}
log.info && log.info(`Using tasks file path: ${tasksJsonPath}`);
const result = await mockRemoveTaskDirect(
{
tasksJsonPath: tasksJsonPath,
id: args.id,
projectRoot: args.projectRoot,
tag: args.tag
},
log,
{ session }
);
if (result.success) {
log.info && log.info(`Successfully removed task: ${args.id}`);
} else {
log.error &&
log.error(`Failed to remove task: ${result.error.message}`);
}
return mockHandleApiResult(
result,
log,
'Error removing task',
undefined,
args.projectRoot
);
} catch (error) {
log.error && log.error(`Error in remove-task tool: ${error.message}`);
return mockCreateErrorResponse(error.message);
}
})
};
// Register the tool with the server
server.addTool(toolConfig);
};
describe('MCP Tool: remove-task', () => {
// Create mock server
let mockServer;
let executeFunction;
// Create mock logger
const mockLogger = {
debug: jest.fn(),
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
};
// Test data
const validArgs = {
id: '5',
projectRoot: '/mock/project/root',
file: '/mock/project/tasks.json',
confirm: true,
tag: 'feature-branch'
};
const multipleTaskArgs = {
id: '5,6.1,7',
projectRoot: '/mock/project/root',
tag: 'master'
};
// Standard responses
const successResponse = {
success: true,
data: {
totalTasks: 1,
successful: 1,
failed: 0,
removedTasks: [
{
id: 5,
title: 'Removed Task',
status: 'pending'
}
],
messages: ["Successfully removed task 5 from tag 'feature-branch'"],
errors: [],
tasksPath: '/mock/project/tasks.json',
tag: 'feature-branch'
}
};
const multipleTasksSuccessResponse = {
success: true,
data: {
totalTasks: 3,
successful: 3,
failed: 0,
removedTasks: [
{ id: 5, title: 'Task 5', status: 'pending' },
{ id: 1, title: 'Subtask 6.1', status: 'done', parentTaskId: 6 },
{ id: 7, title: 'Task 7', status: 'in-progress' }
],
messages: [
"Successfully removed task 5 from tag 'master'",
"Successfully removed subtask 6.1 from tag 'master'",
"Successfully removed task 7 from tag 'master'"
],
errors: [],
tasksPath: '/mock/project/tasks.json',
tag: 'master'
}
};
const errorResponse = {
success: false,
error: {
code: 'INVALID_TASK_ID',
message: "The following tasks were not found in tag 'feature-branch': 999"
}
};
const pathErrorResponse = {
success: false,
error: {
code: 'PATH_ERROR',
message: 'Failed to find tasks.json: No tasks.json found'
}
};
beforeEach(() => {
// Reset all mocks
jest.clearAllMocks();
// Create mock server
mockServer = {
addTool: jest.fn((config) => {
executeFunction = config.execute;
})
};
// Setup default successful response
mockRemoveTaskDirect.mockResolvedValue(successResponse);
mockFindTasksPath.mockReturnValue('/mock/project/tasks.json');
// Register the tool
registerRemoveTaskTool(mockServer);
});
test('should register the tool correctly', () => {
// Verify tool was registered
expect(mockServer.addTool).toHaveBeenCalledWith(
expect.objectContaining({
name: 'remove_task',
description: 'Remove a task or subtask permanently from the tasks list',
parameters: expect.any(Object),
execute: expect.any(Function)
})
);
// Verify the tool config was passed
const toolConfig = mockServer.addTool.mock.calls[0][0];
expect(toolConfig).toHaveProperty('parameters');
expect(toolConfig).toHaveProperty('execute');
});
test('should execute the tool with valid parameters including tag', async () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(validArgs, mockContext);
// Verify findTasksPath was called with correct arguments
expect(mockFindTasksPath).toHaveBeenCalledWith(
{
projectRoot: validArgs.projectRoot,
file: validArgs.file
},
mockLogger
);
// Verify removeTaskDirect was called with correct arguments including tag
expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
tasksJsonPath: '/mock/project/tasks.json',
id: validArgs.id,
projectRoot: validArgs.projectRoot,
tag: validArgs.tag // This is the key test - tag parameter should be passed through
}),
mockLogger,
{
session: mockContext.session
}
);
// Verify handleApiResult was called
expect(mockHandleApiResult).toHaveBeenCalledWith(
successResponse,
mockLogger,
'Error removing task',
undefined,
validArgs.projectRoot
);
});
test('should handle multiple task IDs with tag context', async () => {
// Setup multiple tasks response
mockRemoveTaskDirect.mockResolvedValueOnce(multipleTasksSuccessResponse);
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(multipleTaskArgs, mockContext);
// Verify removeTaskDirect was called with comma-separated IDs and tag
expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
id: '5,6.1,7',
tag: 'master'
}),
mockLogger,
expect.any(Object)
);
// Verify successful handling of multiple tasks
expect(mockHandleApiResult).toHaveBeenCalledWith(
multipleTasksSuccessResponse,
mockLogger,
'Error removing task',
undefined,
multipleTaskArgs.projectRoot
);
});
test('should handle missing tag parameter (defaults to current tag)', async () => {
const argsWithoutTag = {
id: '5',
projectRoot: '/mock/project/root'
};
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(argsWithoutTag, mockContext);
// Verify removeTaskDirect was called with undefined tag (should default to current tag)
expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
id: '5',
projectRoot: '/mock/project/root',
tag: undefined // Should be undefined when not provided
}),
mockLogger,
expect.any(Object)
);
});
test('should handle errors from removeTaskDirect', async () => {
// Setup error response
mockRemoveTaskDirect.mockResolvedValueOnce(errorResponse);
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(validArgs, mockContext);
// Verify removeTaskDirect was called
expect(mockRemoveTaskDirect).toHaveBeenCalled();
// Verify error logging
expect(mockLogger.error).toHaveBeenCalledWith(
"Failed to remove task: The following tasks were not found in tag 'feature-branch': 999"
);
// Verify handleApiResult was called with error response
expect(mockHandleApiResult).toHaveBeenCalledWith(
errorResponse,
mockLogger,
'Error removing task',
undefined,
validArgs.projectRoot
);
});
test('should handle path finding errors', async () => {
// Setup path finding error
mockFindTasksPath.mockImplementationOnce(() => {
throw new Error('No tasks.json found');
});
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
const result = await executeFunction(validArgs, mockContext);
// Verify error logging
expect(mockLogger.error).toHaveBeenCalledWith(
'Error finding tasks.json: No tasks.json found'
);
// Verify error response was returned
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
'Failed to find tasks.json: No tasks.json found'
);
// Verify removeTaskDirect was NOT called
expect(mockRemoveTaskDirect).not.toHaveBeenCalled();
});
test('should handle unexpected errors in execute function', async () => {
// Setup unexpected error
mockRemoveTaskDirect.mockImplementationOnce(() => {
throw new Error('Unexpected error');
});
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(validArgs, mockContext);
// Verify error logging
expect(mockLogger.error).toHaveBeenCalledWith(
'Error in remove-task tool: Unexpected error'
);
// Verify error response was returned
expect(mockCreateErrorResponse).toHaveBeenCalledWith('Unexpected error');
});
test('should properly handle withNormalizedProjectRoot wrapper', () => {
// Verify that withNormalizedProjectRoot was called with the execute function
expect(mockWithNormalizedProjectRoot).toHaveBeenCalledWith(
expect.any(Function)
);
});
test('should log appropriate info messages for successful operations', async () => {
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(validArgs, mockContext);
// Verify appropriate logging
expect(mockLogger.info).toHaveBeenCalledWith(
'Removing task(s) with ID(s): 5'
);
expect(mockLogger.info).toHaveBeenCalledWith(
'Using tasks file path: /mock/project/tasks.json'
);
expect(mockLogger.info).toHaveBeenCalledWith(
'Successfully removed task: 5'
);
});
test('should handle subtask removal with proper tag context', async () => {
const subtaskArgs = {
id: '5.2',
projectRoot: '/mock/project/root',
tag: 'feature-branch'
};
const subtaskSuccessResponse = {
success: true,
data: {
totalTasks: 1,
successful: 1,
failed: 0,
removedTasks: [
{
id: 2,
title: 'Removed Subtask',
status: 'pending',
parentTaskId: 5
}
],
messages: [
"Successfully removed subtask 5.2 from tag 'feature-branch'"
],
errors: [],
tasksPath: '/mock/project/tasks.json',
tag: 'feature-branch'
}
};
mockRemoveTaskDirect.mockResolvedValueOnce(subtaskSuccessResponse);
// Setup context
const mockContext = {
log: mockLogger,
session: { workingDirectory: '/mock/dir' }
};
// Execute the function
await executeFunction(subtaskArgs, mockContext);
// Verify removeTaskDirect was called with subtask ID and tag
expect(mockRemoveTaskDirect).toHaveBeenCalledWith(
expect.objectContaining({
id: '5.2',
tag: 'feature-branch'
}),
mockLogger,
expect.any(Object)
);
// Verify successful handling
expect(mockHandleApiResult).toHaveBeenCalledWith(
subtaskSuccessResponse,
mockLogger,
'Error removing task',
undefined,
subtaskArgs.projectRoot
);
});
});
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/workflow/services/workflow.service.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview WorkflowService - High-level facade for TDD workflow operations
* Provides a simplified API for MCP tools while delegating to WorkflowOrchestrator
*/
import { GitAdapter } from '../../git/adapters/git-adapter.js';
import { WorkflowStateManager } from '../managers/workflow-state-manager.js';
import { WorkflowOrchestrator } from '../orchestrators/workflow-orchestrator.js';
import type {
SubtaskInfo,
TDDPhase,
TestResult,
WorkflowContext,
WorkflowPhase,
WorkflowState
} from '../types.js';
import { WorkflowActivityLogger } from './workflow-activity-logger.js';
/**
* Options for starting a new workflow
*/
export interface StartWorkflowOptions {
taskId: string;
taskTitle: string;
subtasks: Array<{
id: string;
title: string;
status: string;
maxAttempts?: number;
}>;
maxAttempts?: number;
force?: boolean;
tag?: string; // Optional tag for branch naming
}
/**
* Simplified workflow status for MCP responses
*/
export interface WorkflowStatus {
taskId: string;
phase: WorkflowPhase;
tddPhase?: TDDPhase;
branchName?: string;
currentSubtask?: {
id: string;
title: string;
attempts: number;
maxAttempts: number;
};
progress: {
completed: number;
total: number;
current: number;
percentage: number;
};
}
/**
* Next action recommendation for AI agent
*/
export interface NextAction {
action: string;
description: string;
nextSteps: string;
phase: WorkflowPhase;
tddPhase?: TDDPhase;
subtask?: {
id: string;
title: string;
};
}
/**
* WorkflowService - Facade for workflow operations
* Manages WorkflowOrchestrator lifecycle and state persistence
*/
export class WorkflowService {
private readonly projectRoot: string;
private readonly stateManager: WorkflowStateManager;
private orchestrator?: WorkflowOrchestrator;
private activityLogger?: WorkflowActivityLogger;
constructor(projectRoot: string) {
this.projectRoot = projectRoot;
this.stateManager = new WorkflowStateManager(projectRoot);
}
/**
* Check if workflow state exists
*/
async hasWorkflow(): Promise<boolean> {
return await this.stateManager.exists();
}
/**
* Start a new TDD workflow
*/
async startWorkflow(options: StartWorkflowOptions): Promise<WorkflowStatus> {
const {
taskId,
taskTitle,
subtasks,
maxAttempts = 3,
force,
tag
} = options;
// Check for existing workflow
if ((await this.hasWorkflow()) && !force) {
throw new Error(
'Workflow already exists. Use force=true to override or resume existing workflow.'
);
}
// Initialize git adapter and ensure clean state
const gitAdapter = new GitAdapter(this.projectRoot);
await gitAdapter.ensureGitRepository();
await gitAdapter.ensureCleanWorkingTree();
// Parse subtasks to WorkflowContext format
const workflowSubtasks: SubtaskInfo[] = subtasks.map((st) => ({
id: st.id,
title: st.title,
status: st.status === 'done' ? 'completed' : 'pending',
attempts: 0,
maxAttempts: st.maxAttempts || maxAttempts
}));
// Find the first incomplete subtask to resume from
const firstIncompleteIndex = workflowSubtasks.findIndex(
(st) => st.status !== 'completed'
);
// If all subtasks are already completed, throw an error
if (firstIncompleteIndex === -1) {
throw new Error(
`All subtasks for task ${taskId} are already completed. Nothing to do.`
);
}
// Create workflow context, starting from first incomplete subtask
const context: WorkflowContext = {
taskId,
subtasks: workflowSubtasks,
currentSubtaskIndex: firstIncompleteIndex,
errors: [],
metadata: {
startedAt: new Date().toISOString(),
taskTitle,
resumedFromSubtask:
firstIncompleteIndex > 0
? workflowSubtasks[firstIncompleteIndex].id
: undefined
}
};
// Create orchestrator with auto-persistence
this.orchestrator = new WorkflowOrchestrator(context);
this.orchestrator.enableAutoPersist(async (state: WorkflowState) => {
await this.stateManager.save(state);
});
// Initialize activity logger to track all workflow events
this.activityLogger = new WorkflowActivityLogger(
this.orchestrator,
this.stateManager.getActivityLogPath()
);
this.activityLogger.start();
// Transition through PREFLIGHT and BRANCH_SETUP phases
await this.orchestrator.transition({ type: 'PREFLIGHT_COMPLETE' });
// Create git branch with descriptive name
const branchName = this.generateBranchName(taskId, taskTitle, tag);
// Check if we're already on the target branch
const currentBranch = await gitAdapter.getCurrentBranch();
if (currentBranch !== branchName) {
// Only create branch if we're not already on it
await gitAdapter.createAndCheckoutBranch(branchName);
}
// Transition to SUBTASK_LOOP with RED phase
await this.orchestrator.transition({
type: 'BRANCH_CREATED',
branchName
});
return this.getStatus();
}
/**
* Resume an existing workflow
*/
async resumeWorkflow(): Promise<WorkflowStatus> {
// Load state
const state = await this.stateManager.load();
// Create new orchestrator with loaded context
this.orchestrator = new WorkflowOrchestrator(state.context);
// Validate and restore state
if (!this.orchestrator.canResumeFromState(state)) {
throw new Error(
'Invalid workflow state. State may be corrupted. Consider starting a new workflow.'
);
}
this.orchestrator.restoreState(state);
// Re-enable auto-persistence
this.orchestrator.enableAutoPersist(async (newState: WorkflowState) => {
await this.stateManager.save(newState);
});
// Initialize activity logger to continue tracking events
this.activityLogger = new WorkflowActivityLogger(
this.orchestrator,
this.stateManager.getActivityLogPath()
);
this.activityLogger.start();
return this.getStatus();
}
/**
* Get current workflow status
*/
getStatus(): WorkflowStatus {
if (!this.orchestrator) {
throw new Error('No active workflow. Start or resume a workflow first.');
}
const context = this.orchestrator.getContext();
const progress = this.orchestrator.getProgress();
const currentSubtask = this.orchestrator.getCurrentSubtask();
return {
taskId: context.taskId,
phase: this.orchestrator.getCurrentPhase(),
tddPhase: this.orchestrator.getCurrentTDDPhase(),
branchName: context.branchName,
currentSubtask: currentSubtask
? {
id: currentSubtask.id,
title: currentSubtask.title,
attempts: currentSubtask.attempts,
maxAttempts: currentSubtask.maxAttempts || 3
}
: undefined,
progress
};
}
/**
* Get workflow context (for accessing full state details)
*/
getContext(): WorkflowContext {
if (!this.orchestrator) {
throw new Error('No active workflow. Start or resume a workflow first.');
}
return this.orchestrator.getContext();
}
/**
* Get next recommended action for AI agent
*/
getNextAction(): NextAction {
if (!this.orchestrator) {
throw new Error('No active workflow. Start or resume a workflow first.');
}
const phase = this.orchestrator.getCurrentPhase();
const tddPhase = this.orchestrator.getCurrentTDDPhase();
const currentSubtask = this.orchestrator.getCurrentSubtask();
// Determine action based on current phase
if (phase === 'COMPLETE') {
return {
action: 'workflow_complete',
description: 'All subtasks completed',
nextSteps:
'All subtasks completed! Review the entire implementation and merge your branch when ready.',
phase
};
}
if (phase === 'FINALIZE') {
return {
action: 'finalize_workflow',
description: 'Finalize and complete the workflow',
nextSteps:
'All subtasks are complete! Use autopilot_finalize to verify no uncommitted changes remain and mark the workflow as complete.',
phase
};
}
if (phase !== 'SUBTASK_LOOP' || !tddPhase || !currentSubtask) {
return {
action: 'unknown',
description: 'Workflow is not in active state',
nextSteps: 'Use autopilot_status to check workflow state.',
phase
};
}
const baseAction = {
phase,
tddPhase,
subtask: {
id: currentSubtask.id,
title: currentSubtask.title
}
};
switch (tddPhase) {
case 'RED':
return {
...baseAction,
action: 'generate_test',
description: 'Generate failing test for current subtask',
nextSteps: `Write failing tests for subtask ${currentSubtask.id}: "${currentSubtask.title}". Create test file(s) that validate the expected behavior. Run tests and use autopilot_complete_phase with results. Note: If all tests pass (0 failures), the feature is already implemented and the subtask will be auto-completed.`
};
case 'GREEN':
return {
...baseAction,
action: 'implement_code',
description: 'Implement feature to make tests pass',
nextSteps: `Implement code to make tests pass for subtask ${currentSubtask.id}: "${currentSubtask.title}". Write the minimal code needed to pass all tests (GREEN phase), then use autopilot_complete_phase with test results.`
};
case 'COMMIT':
return {
...baseAction,
action: 'commit_changes',
description: 'Commit RED-GREEN cycle changes',
nextSteps: `Review and commit your changes for subtask ${currentSubtask.id}: "${currentSubtask.title}". Use autopilot_commit to create the commit and advance to the next subtask.`
};
default:
return {
...baseAction,
action: 'unknown',
description: 'Unknown TDD phase',
nextSteps: 'Use autopilot_status to check workflow state.'
};
}
}
/**
* Complete current TDD phase with test results
*/
async completePhase(testResults: TestResult): Promise<WorkflowStatus> {
if (!this.orchestrator) {
throw new Error('No active workflow. Start or resume a workflow first.');
}
const tddPhase = this.orchestrator.getCurrentTDDPhase();
if (!tddPhase) {
throw new Error('Not in active TDD phase');
}
// Transition based on current phase
switch (tddPhase) {
case 'RED':
await this.orchestrator.transition({
type: 'RED_PHASE_COMPLETE',
testResults
});
break;
case 'GREEN':
await this.orchestrator.transition({
type: 'GREEN_PHASE_COMPLETE',
testResults
});
break;
case 'COMMIT':
throw new Error(
'Cannot complete COMMIT phase with test results. Use commit() instead.'
);
default:
throw new Error(`Unknown TDD phase: ${tddPhase}`);
}
return this.getStatus();
}
/**
* Commit current changes and advance workflow
*/
async commit(): Promise<WorkflowStatus> {
if (!this.orchestrator) {
throw new Error('No active workflow. Start or resume a workflow first.');
}
const tddPhase = this.orchestrator.getCurrentTDDPhase();
if (tddPhase !== 'COMMIT') {
throw new Error(
`Cannot commit in ${tddPhase} phase. Complete RED and GREEN phases first.`
);
}
// Transition COMMIT phase complete
await this.orchestrator.transition({
type: 'COMMIT_COMPLETE'
});
// Check if should advance to next subtask
const progress = this.orchestrator.getProgress();
if (progress.current < progress.total) {
await this.orchestrator.transition({ type: 'SUBTASK_COMPLETE' });
} else {
// All subtasks complete
await this.orchestrator.transition({ type: 'ALL_SUBTASKS_COMPLETE' });
}
return this.getStatus();
}
/**
* Finalize and complete the workflow
* Validates working tree is clean before marking complete
*/
async finalizeWorkflow(): Promise<WorkflowStatus> {
if (!this.orchestrator) {
throw new Error('No active workflow. Start or resume a workflow first.');
}
const phase = this.orchestrator.getCurrentPhase();
if (phase !== 'FINALIZE') {
throw new Error(
`Cannot finalize workflow in ${phase} phase. Complete all subtasks first.`
);
}
// Check working tree is clean
const gitAdapter = new GitAdapter(this.projectRoot);
const statusSummary = await gitAdapter.getStatusSummary();
if (!statusSummary.isClean) {
throw new Error(
`Cannot finalize workflow: working tree has uncommitted changes.\n` +
`Staged: ${statusSummary.staged}, Modified: ${statusSummary.modified}, ` +
`Deleted: ${statusSummary.deleted}, Untracked: ${statusSummary.untracked}\n` +
`Please commit all changes before finalizing the workflow.`
);
}
// Transition to COMPLETE
await this.orchestrator.transition({ type: 'FINALIZE_COMPLETE' });
return this.getStatus();
}
/**
* Abort current workflow
*/
async abortWorkflow(): Promise<void> {
if (this.orchestrator) {
await this.orchestrator.transition({ type: 'ABORT' });
}
// Delete state file
await this.stateManager.delete();
this.orchestrator = undefined;
}
/**
* Generate a descriptive git branch name
* Format: tag-name/task-id-task-title or task-id-task-title
*/
private generateBranchName(
taskId: string,
taskTitle: string,
tag?: string
): string {
// Sanitize task title for branch name
const sanitizedTitle = taskTitle
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with dash
.replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
.substring(0, 50); // Limit length
// Format task ID for branch name
const formattedTaskId = taskId.replace(/\./g, '-');
// Add tag prefix if tag is provided
const tagPrefix = tag ? `${tag}/` : '';
return `${tagPrefix}task-${formattedTaskId}-${sanitizedTitle}`;
}
}
```
--------------------------------------------------------------------------------
/packages/ai-sdk-provider-grok-cli/src/grok-cli-language-model.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Grok CLI Language Model implementation for AI SDK v5
*/
import { spawn } from 'child_process';
import { promises as fs } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import type {
LanguageModelV2,
LanguageModelV2CallOptions,
LanguageModelV2CallWarning
} from '@ai-sdk/provider';
import { NoSuchModelError } from '@ai-sdk/provider';
import { generateId } from '@ai-sdk/provider-utils';
import {
createAPICallError,
createAuthenticationError,
createInstallationError,
createTimeoutError
} from './errors.js';
import { extractJson } from './json-extractor.js';
import {
convertFromGrokCliResponse,
createPromptFromMessages,
escapeShellArg
} from './message-converter.js';
import type {
GrokCliLanguageModelOptions,
GrokCliModelId,
GrokCliSettings
} from './types.js';
/**
* Grok CLI Language Model implementation for AI SDK v5
*/
export class GrokCliLanguageModel implements LanguageModelV2 {
readonly specificationVersion = 'v2' as const;
readonly defaultObjectGenerationMode = 'json' as const;
readonly supportsImageUrls = false;
readonly supportsStructuredOutputs = false;
readonly supportedUrls: Record<string, RegExp[]> = {};
readonly modelId: GrokCliModelId;
readonly settings: GrokCliSettings;
constructor(options: GrokCliLanguageModelOptions) {
this.modelId = options.id;
this.settings = options.settings ?? {};
// Validate model ID format
if (
!this.modelId ||
typeof this.modelId !== 'string' ||
this.modelId.trim() === ''
) {
throw new NoSuchModelError({
modelId: this.modelId,
modelType: 'languageModel'
});
}
}
get provider(): string {
return 'grok-cli';
}
/**
* Check if Grok CLI is installed and available
*/
private async checkGrokCliInstallation(): Promise<boolean> {
return new Promise((resolve) => {
const child = spawn('grok', ['--version'], {
stdio: 'pipe'
});
child.on('error', () => resolve(false));
child.on('exit', (code) => resolve(code === 0));
});
}
/**
* Get API key from settings or environment
*/
private async getApiKey(): Promise<string | null> {
// Check settings first
if (this.settings.apiKey) {
return this.settings.apiKey;
}
// Check environment variable
if (process.env.GROK_CLI_API_KEY) {
return process.env.GROK_CLI_API_KEY;
}
// Check grok-cli config file
try {
const configPath = join(homedir(), '.grok', 'user-settings.json');
const configContent = await fs.readFile(configPath, 'utf8');
const config = JSON.parse(configContent);
return config.apiKey || null;
} catch (error) {
return null;
}
}
/**
* Execute Grok CLI command
*/
private async executeGrokCli(
args: string[],
options: { timeout?: number; apiKey?: string } = {}
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
// Default timeout based on model type
let defaultTimeout = 120000; // 2 minutes default
if (this.modelId.includes('grok-4')) {
defaultTimeout = 600000; // 10 minutes for grok-4 models (they seem to hang during setup)
}
const timeout = options.timeout ?? this.settings.timeout ?? defaultTimeout;
return new Promise((resolve, reject) => {
const child = spawn('grok', args, {
stdio: 'pipe',
cwd: this.settings.workingDirectory || process.cwd(),
env:
options.apiKey === undefined
? process.env
: { ...process.env, GROK_CLI_API_KEY: options.apiKey }
});
let stdout = '';
let stderr = '';
let timeoutId: NodeJS.Timeout | undefined;
// Set up timeout
if (timeout > 0) {
timeoutId = setTimeout(() => {
child.kill('SIGTERM');
reject(
createTimeoutError({
message: `Grok CLI command timed out after ${timeout}ms`,
timeoutMs: timeout,
promptExcerpt: args.join(' ').substring(0, 200)
})
);
}, timeout);
}
child.stdout?.on('data', (data) => {
const chunk = data.toString();
stdout += chunk;
});
child.stderr?.on('data', (data) => {
const chunk = data.toString();
stderr += chunk;
});
child.on('error', (error) => {
if (timeoutId) clearTimeout(timeoutId);
if ((error as any).code === 'ENOENT') {
reject(createInstallationError({}));
} else {
reject(
createAPICallError({
message: `Failed to execute Grok CLI: ${error.message}`,
code: (error as any).code,
stderr: error.message,
isRetryable: false
})
);
}
});
child.on('exit', (exitCode) => {
if (timeoutId) clearTimeout(timeoutId);
resolve({
stdout: stdout.trim(),
stderr: stderr.trim(),
exitCode: exitCode || 0
});
});
});
}
/**
* Generate comprehensive warnings for unsupported parameters and validation issues
*/
private generateAllWarnings(
options: LanguageModelV2CallOptions,
prompt: string
): LanguageModelV2CallWarning[] {
const warnings: LanguageModelV2CallWarning[] = [];
const unsupportedParams: string[] = [];
// Check for unsupported parameters
if (options.temperature !== undefined)
unsupportedParams.push('temperature');
if (options.topP !== undefined) unsupportedParams.push('topP');
if (options.topK !== undefined) unsupportedParams.push('topK');
if (options.presencePenalty !== undefined)
unsupportedParams.push('presencePenalty');
if (options.frequencyPenalty !== undefined)
unsupportedParams.push('frequencyPenalty');
if (options.stopSequences !== undefined && options.stopSequences.length > 0)
unsupportedParams.push('stopSequences');
if (options.seed !== undefined) unsupportedParams.push('seed');
if (unsupportedParams.length > 0) {
// Add a warning for each unsupported parameter
for (const param of unsupportedParams) {
warnings.push({
type: 'unsupported-setting',
setting: param as
| 'temperature'
| 'topP'
| 'topK'
| 'presencePenalty'
| 'frequencyPenalty'
| 'stopSequences'
| 'seed',
details: `Grok CLI does not support the ${param} parameter. It will be ignored.`
});
}
}
// Add model validation warnings if needed
if (!this.modelId || this.modelId.trim() === '') {
warnings.push({
type: 'other',
message: 'Model ID is empty or invalid'
});
}
// Add prompt validation
if (!prompt || prompt.trim() === '') {
warnings.push({
type: 'other',
message: 'Prompt is empty'
});
}
return warnings;
}
/**
* Generate text using Grok CLI
*/
async doGenerate(options: LanguageModelV2CallOptions) {
// Handle abort signal early
if (options.abortSignal?.aborted) {
throw options.abortSignal.reason || new Error('Request aborted');
}
// Check CLI installation
const isInstalled = await this.checkGrokCliInstallation();
if (!isInstalled) {
throw createInstallationError({});
}
// Get API key
const apiKey = await this.getApiKey();
if (!apiKey) {
throw createAuthenticationError({
message:
'Grok CLI API key not found. Set GROK_CLI_API_KEY environment variable or configure grok-cli.'
});
}
const prompt = createPromptFromMessages(options.prompt);
const warnings = this.generateAllWarnings(options, prompt);
// Build command arguments
const args = ['--prompt', escapeShellArg(prompt)];
// Add model if specified
if (this.modelId && this.modelId !== 'default') {
args.push('--model', this.modelId);
}
// Skip API key parameter if it's likely already configured to avoid hanging
// The CLI seems to hang when trying to save API keys for grok-4 models
// if (apiKey) {
// args.push('--api-key', apiKey);
// }
// Add base URL if provided in settings
if (this.settings.baseURL) {
args.push('--base-url', this.settings.baseURL);
}
// Add working directory if specified
if (this.settings.workingDirectory) {
args.push('--directory', this.settings.workingDirectory);
}
try {
const result = await this.executeGrokCli(args, { apiKey });
if (result.exitCode !== 0) {
// Handle authentication errors
if (
result.stderr.toLowerCase().includes('unauthorized') ||
result.stderr.toLowerCase().includes('authentication')
) {
throw createAuthenticationError({
message: `Grok CLI authentication failed: ${result.stderr}`
});
}
throw createAPICallError({
message: `Grok CLI failed with exit code ${result.exitCode}: ${result.stderr || 'Unknown error'}`,
exitCode: result.exitCode,
stderr: result.stderr,
stdout: result.stdout,
promptExcerpt: prompt.substring(0, 200),
isRetryable: false
});
}
// Parse response
const response = convertFromGrokCliResponse(result.stdout);
let text = response.text || '';
// Extract JSON if in object-json mode
const isObjectJson = (
o: unknown
): o is { mode: { type: 'object-json' } } =>
!!o &&
typeof o === 'object' &&
'mode' in o &&
(o as any).mode?.type === 'object-json';
if (isObjectJson(options) && text) {
text = extractJson(text);
}
return {
content: [
{
type: 'text' as const,
text: text || ''
}
],
usage: response.usage
? {
inputTokens: response.usage.promptTokens,
outputTokens: response.usage.completionTokens,
totalTokens: response.usage.totalTokens
}
: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
finishReason: 'stop' as const,
rawCall: {
rawPrompt: prompt,
rawSettings: args
},
warnings: warnings,
response: {
id: generateId(),
timestamp: new Date(),
modelId: this.modelId
},
request: {
body: prompt
},
providerMetadata: {
'grok-cli': {
exitCode: result.exitCode,
...(result.stderr && { stderr: result.stderr })
}
}
};
} catch (error) {
// Re-throw our custom errors
if (
(error as any).name === 'APICallError' ||
(error as any).name === 'LoadAPIKeyError'
) {
throw error;
}
// Wrap other errors
throw createAPICallError({
message: `Grok CLI execution failed: ${(error as Error).message}`,
code: (error as any).code,
promptExcerpt: prompt.substring(0, 200),
isRetryable: false
});
}
}
/**
* Stream text using Grok CLI
* Note: Grok CLI doesn't natively support streaming, so this simulates streaming
* by generating the full response and then streaming it in chunks
*/
async doStream(options: LanguageModelV2CallOptions) {
const prompt = createPromptFromMessages(options.prompt);
const warnings = this.generateAllWarnings(options, prompt);
const stream = new ReadableStream({
start: async (controller) => {
let abortListener: (() => void) | undefined;
try {
// Handle abort signal
if (options.abortSignal?.aborted) {
throw options.abortSignal.reason || new Error('Request aborted');
}
// Set up abort listener
if (options.abortSignal) {
abortListener = () => {
controller.enqueue({
type: 'error',
error:
options.abortSignal?.reason || new Error('Request aborted')
});
controller.close();
};
options.abortSignal.addEventListener('abort', abortListener, {
once: true
});
}
// Emit stream-start with warnings
controller.enqueue({ type: 'stream-start', warnings });
// Generate the full response first
const result = await this.doGenerate(options);
// Emit response metadata
controller.enqueue({
type: 'response-metadata',
id: result.response.id,
timestamp: result.response.timestamp,
modelId: result.response.modelId
});
// Simulate streaming by chunking the text
const content = result.content || [];
const text =
content.length > 0 && content[0].type === 'text'
? content[0].text
: '';
const chunkSize = 50; // Characters per chunk
let textPartId: string | undefined;
// Emit text-start if we have content
if (text.length > 0) {
textPartId = generateId();
controller.enqueue({
type: 'text-start',
id: textPartId
});
}
for (let i = 0; i < text.length; i += chunkSize) {
// Check for abort during streaming
if (options.abortSignal?.aborted) {
throw options.abortSignal.reason || new Error('Request aborted');
}
const chunk = text.slice(i, i + chunkSize);
controller.enqueue({
type: 'text-delta',
id: textPartId!,
delta: chunk
});
// Add small delay to simulate streaming
await new Promise((resolve) => setTimeout(resolve, 20));
}
// Close text part if opened
if (textPartId) {
controller.enqueue({
type: 'text-end',
id: textPartId
});
}
// Emit finish event
controller.enqueue({
type: 'finish',
finishReason: result.finishReason,
usage: result.usage,
providerMetadata: result.providerMetadata
});
controller.close();
} catch (error) {
controller.enqueue({
type: 'error',
error
});
controller.close();
} finally {
// Clean up abort listener
if (options.abortSignal && abortListener) {
options.abortSignal.removeEventListener('abort', abortListener);
}
}
},
cancel: () => {
// Clean up if stream is cancelled
}
});
return {
stream,
request: {
body: prompt
}
};
}
}
```