This is page 30 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/apps/cli/src/ui/components/task-detail.component.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Task detail component for show command
3 | * Displays detailed task information in a structured format
4 | */
5 |
6 | import type { StorageType, Subtask, Task } from '@tm/core';
7 | import boxen from 'boxen';
8 | import chalk from 'chalk';
9 | import Table from 'cli-table3';
10 | import { MarkedExtension, marked } from 'marked';
11 | import { markedTerminal } from 'marked-terminal';
12 | import {
13 | getComplexityWithColor,
14 | getPriorityWithColor,
15 | getStatusWithColor
16 | } from '../../utils/ui.js';
17 |
18 | // Configure marked to use terminal renderer with subtle colors
19 | marked.use(
20 | markedTerminal({
21 | // More subtle colors that match the overall design
22 | code: (code: string) => {
23 | // Custom code block handler to preserve formatting
24 | return code
25 | .split('\n')
26 | .map((line) => ' ' + chalk.cyan(line))
27 | .join('\n');
28 | },
29 | blockquote: chalk.gray.italic,
30 | html: chalk.gray,
31 | heading: chalk.white.bold, // White bold for headings
32 | hr: chalk.gray,
33 | listitem: chalk.white, // White for list items
34 | paragraph: chalk.white, // White for paragraphs (default text color)
35 | strong: chalk.white.bold, // White bold for strong text
36 | em: chalk.white.italic, // White italic for emphasis
37 | codespan: chalk.cyan, // Cyan for inline code (no background)
38 | del: chalk.dim.strikethrough,
39 | link: chalk.blue,
40 | href: chalk.blue.underline,
41 | // Add more explicit code block handling
42 | showSectionPrefix: false,
43 | unescape: true,
44 | emoji: false,
45 | // Try to preserve whitespace in code blocks
46 | tab: 4,
47 | width: 120
48 | }) as MarkedExtension
49 | );
50 |
51 | // Also set marked options to preserve whitespace
52 | marked.setOptions({
53 | breaks: true,
54 | gfm: true
55 | });
56 |
57 | /**
58 | * Display the task header with tag
59 | */
60 | export function displayTaskHeader(
61 | taskId: string | number,
62 | title: string
63 | ): void {
64 | // Display task header box
65 | console.log(
66 | boxen(chalk.white.bold(`Task: #${taskId} - ${title}`), {
67 | padding: { top: 0, bottom: 0, left: 1, right: 1 },
68 | borderColor: 'blue',
69 | borderStyle: 'round'
70 | })
71 | );
72 | }
73 |
74 | /**
75 | * Display task properties in a table format
76 | */
77 | export function displayTaskProperties(
78 | task: Task | Subtask,
79 | originalTaskId?: string
80 | ): void {
81 | const terminalWidth = process.stdout.columns * 0.95 || 100;
82 | // Create table for task properties - simple 2-column layout
83 | const table = new Table({
84 | head: [],
85 | style: {
86 | head: [],
87 | border: ['grey']
88 | },
89 | colWidths: [
90 | Math.floor(terminalWidth * 0.2),
91 | Math.floor(terminalWidth * 0.8)
92 | ],
93 | wordWrap: true
94 | });
95 |
96 | const deps =
97 | task.dependencies && task.dependencies.length > 0
98 | ? task.dependencies.map((d) => String(d)).join(', ')
99 | : 'None';
100 |
101 | // Use originalTaskId if provided (for subtasks like "104.1")
102 | const displayId = originalTaskId || String(task.id);
103 |
104 | // Build the left column (labels) and right column (values)
105 | const labels = [
106 | chalk.cyan('ID:'),
107 | chalk.cyan('Title:'),
108 | chalk.cyan('Status:'),
109 | chalk.cyan('Priority:'),
110 | chalk.cyan('Dependencies:'),
111 | chalk.cyan('Complexity:'),
112 | chalk.cyan('Description:')
113 | ].join('\n');
114 |
115 | const values = [
116 | displayId,
117 | task.title,
118 | getStatusWithColor(task.status),
119 | getPriorityWithColor(task.priority),
120 | deps,
121 | typeof task.complexity === 'number'
122 | ? getComplexityWithColor(task.complexity)
123 | : chalk.gray('N/A'),
124 | task.description || ''
125 | ].join('\n');
126 |
127 | table.push([labels, values]);
128 |
129 | console.log(table.toString());
130 | }
131 |
132 | /**
133 | * Display implementation details in a box
134 | */
135 | export function displayImplementationDetails(details: string): void {
136 | // Handle all escaped characters properly
137 | const cleanDetails = details
138 | .replace(/\\n/g, '\n') // Convert \n to actual newlines
139 | .replace(/\\t/g, '\t') // Convert \t to actual tabs
140 | .replace(/\\"/g, '"') // Convert \" to actual quotes
141 | .replace(/\\\\/g, '\\'); // Convert \\ to single backslash
142 |
143 | const terminalWidth = process.stdout.columns * 0.95 || 100;
144 |
145 | // Parse markdown to terminal-friendly format
146 | const markdownResult = marked(cleanDetails);
147 | const formattedDetails =
148 | typeof markdownResult === 'string' ? markdownResult.trim() : cleanDetails; // Fallback to original if Promise
149 |
150 | console.log(
151 | boxen(
152 | chalk.white.bold('Implementation Details:') + '\n\n' + formattedDetails,
153 | {
154 | padding: 1,
155 | borderStyle: 'round',
156 | borderColor: 'cyan', // Changed to cyan to match the original
157 | width: terminalWidth // Fixed width to match the original
158 | }
159 | )
160 | );
161 | }
162 |
163 | /**
164 | * Display test strategy in a box
165 | */
166 | export function displayTestStrategy(testStrategy: string): void {
167 | // Handle all escaped characters properly (same as implementation details)
168 | const cleanStrategy = testStrategy
169 | .replace(/\\n/g, '\n') // Convert \n to actual newlines
170 | .replace(/\\t/g, '\t') // Convert \t to actual tabs
171 | .replace(/\\"/g, '"') // Convert \" to actual quotes
172 | .replace(/\\\\/g, '\\'); // Convert \\ to single backslash
173 |
174 | const terminalWidth = process.stdout.columns * 0.95 || 100;
175 |
176 | // Parse markdown to terminal-friendly format (same as implementation details)
177 | const markdownResult = marked(cleanStrategy);
178 | const formattedStrategy =
179 | typeof markdownResult === 'string' ? markdownResult.trim() : cleanStrategy; // Fallback to original if Promise
180 |
181 | console.log(
182 | boxen(chalk.white.bold('Test Strategy:') + '\n\n' + formattedStrategy, {
183 | padding: 1,
184 | borderStyle: 'round',
185 | borderColor: 'cyan', // Changed to cyan to match implementation details
186 | width: terminalWidth
187 | })
188 | );
189 | }
190 |
191 | /**
192 | * Display subtasks in a table format
193 | */
194 | export function displaySubtasks(
195 | subtasks: Array<{
196 | id: string | number;
197 | title: string;
198 | status: any;
199 | description?: string;
200 | dependencies?: string[];
201 | }>,
202 | parentTaskId?: string | number,
203 | storageType?: Exclude<StorageType, 'auto'>
204 | ): void {
205 | const terminalWidth = process.stdout.columns * 0.95 || 100;
206 | // Display subtasks header
207 | console.log(
208 | boxen(chalk.magenta.bold('Subtasks'), {
209 | padding: { top: 0, bottom: 0, left: 1, right: 1 },
210 | borderColor: 'magenta',
211 | borderStyle: 'round',
212 | margin: { top: 1, bottom: 0 }
213 | })
214 | );
215 |
216 | // Create subtasks table
217 | const table = new Table({
218 | head: [
219 | chalk.magenta.bold('ID'),
220 | chalk.magenta.bold('Status'),
221 | chalk.magenta.bold('Title'),
222 | chalk.magenta.bold('Deps')
223 | ],
224 | style: {
225 | head: [],
226 | border: ['grey']
227 | },
228 | colWidths: [
229 | Math.floor(terminalWidth * 0.1),
230 | Math.floor(terminalWidth * 0.15),
231 | Math.floor(terminalWidth * 0.6),
232 | Math.floor(terminalWidth * 0.15)
233 | ],
234 | wordWrap: true
235 | });
236 |
237 | subtasks.forEach((subtask) => {
238 | // Format subtask ID based on storage type:
239 | // - File storage: Show parent prefix (e.g., 10.1, 10.2)
240 | // - API storage: Show subtask ID only (e.g., 1, 2)
241 | const subtaskId =
242 | storageType === 'file' && parentTaskId
243 | ? `${parentTaskId}.${subtask.id}`
244 | : String(subtask.id);
245 |
246 | // Format dependencies
247 | const deps =
248 | subtask.dependencies && subtask.dependencies.length > 0
249 | ? subtask.dependencies.join(', ')
250 | : 'None';
251 |
252 | table.push([
253 | subtaskId,
254 | getStatusWithColor(subtask.status),
255 | subtask.title,
256 | deps
257 | ]);
258 | });
259 |
260 | console.log(table.toString());
261 | }
262 |
263 | /**
264 | * Display suggested actions
265 | */
266 | export function displaySuggestedActions(taskId: string | number): void {
267 | console.log(
268 | boxen(
269 | chalk.white.bold('Suggested Actions:') +
270 | '\n\n' +
271 | `${chalk.cyan('1.')} Run ${chalk.yellow(`task-master set-status --id=${taskId} --status=in-progress`)} to start working\n` +
272 | `${chalk.cyan('2.')} Run ${chalk.yellow(`task-master expand --id=${taskId}`)} to break down into subtasks\n` +
273 | `${chalk.cyan('3.')} Run ${chalk.yellow(`task-master update-task --id=${taskId} --prompt="..."`)} to update details`,
274 | {
275 | padding: 1,
276 | margin: { top: 1 },
277 | borderStyle: 'round',
278 | borderColor: 'green',
279 | width: process.stdout.columns * 0.95 || 100
280 | }
281 | )
282 | );
283 | }
284 |
285 | /**
286 | * Display complete task details - used by both show and start commands
287 | */
288 | export function displayTaskDetails(
289 | task: Task | Subtask,
290 | options?: {
291 | statusFilter?: string;
292 | showSuggestedActions?: boolean;
293 | customHeader?: string;
294 | headerColor?: string;
295 | originalTaskId?: string;
296 | storageType?: Exclude<StorageType, 'auto'>;
297 | }
298 | ): void {
299 | const {
300 | statusFilter,
301 | showSuggestedActions = false,
302 | customHeader,
303 | headerColor = 'blue',
304 | originalTaskId,
305 | storageType
306 | } = options || {};
307 |
308 | // Display header - either custom or default
309 | if (customHeader) {
310 | console.log(
311 | boxen(chalk.white.bold(customHeader), {
312 | padding: { top: 0, bottom: 0, left: 1, right: 1 },
313 | borderColor: headerColor,
314 | borderStyle: 'round',
315 | margin: { top: 1 }
316 | })
317 | );
318 | } else {
319 | // Use originalTaskId if provided (for subtasks like "104.1")
320 | const displayId = originalTaskId || task.id;
321 | displayTaskHeader(displayId, task.title);
322 | }
323 |
324 | // Display task properties in table format
325 | displayTaskProperties(task, originalTaskId);
326 |
327 | // Display implementation details if available
328 | if (task.details) {
329 | console.log(); // Empty line for spacing
330 | displayImplementationDetails(task.details);
331 | }
332 |
333 | // Display test strategy if available
334 | if ('testStrategy' in task && task.testStrategy) {
335 | console.log(); // Empty line for spacing
336 | displayTestStrategy(task.testStrategy as string);
337 | }
338 |
339 | // Display subtasks if available
340 | if (task.subtasks && task.subtasks.length > 0) {
341 | // Filter subtasks by status if provided
342 | const filteredSubtasks = statusFilter
343 | ? task.subtasks.filter((sub) => sub.status === statusFilter)
344 | : task.subtasks;
345 |
346 | if (filteredSubtasks.length === 0 && statusFilter) {
347 | console.log(); // Empty line for spacing
348 | console.log(chalk.gray(` No subtasks with status '${statusFilter}'`));
349 | } else if (filteredSubtasks.length > 0) {
350 | console.log(); // Empty line for spacing
351 | displaySubtasks(filteredSubtasks, task.id, storageType);
352 | }
353 | }
354 |
355 | // Display suggested actions if requested
356 | if (showSuggestedActions) {
357 | console.log(); // Empty line for spacing
358 | const actionTaskId = originalTaskId || task.id;
359 | displaySuggestedActions(actionTaskId);
360 | }
361 | }
362 |
```
--------------------------------------------------------------------------------
/src/utils/profiles.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Profiles Utility
3 | * Consolidated utilities for profile detection, setup, and summary generation
4 | */
5 | import fs from 'fs';
6 | import path from 'path';
7 | import inquirer from 'inquirer';
8 | import chalk from 'chalk';
9 | import boxen from 'boxen';
10 | import { log } from '../../scripts/modules/utils.js';
11 | import { getRulesProfile } from './rule-transformer.js';
12 | import { RULE_PROFILES } from '../constants/profiles.js';
13 |
14 | // =============================================================================
15 | // PROFILE DETECTION
16 | // =============================================================================
17 |
18 | /**
19 | * Get the display name for a profile
20 | * @param {string} profileName - The profile name
21 | * @returns {string} - The display name
22 | */
23 | export function getProfileDisplayName(profileName) {
24 | try {
25 | const profile = getRulesProfile(profileName);
26 | return profile.displayName || profileName;
27 | } catch (error) {
28 | return profileName;
29 | }
30 | }
31 |
32 | /**
33 | * Get installed profiles in the project directory
34 | * @param {string} projectRoot - Project directory path
35 | * @returns {string[]} - Array of installed profile names
36 | */
37 | export function getInstalledProfiles(projectRoot) {
38 | const installedProfiles = [];
39 |
40 | for (const profileName of RULE_PROFILES) {
41 | try {
42 | const profile = getRulesProfile(profileName);
43 | const profileDir = path.join(projectRoot, profile.profileDir);
44 |
45 | // Check if profile directory exists (skip root directory check)
46 | if (profile.profileDir === '.' || fs.existsSync(profileDir)) {
47 | // Check if any files from the profile's fileMap exist
48 | const rulesDir = path.join(projectRoot, profile.rulesDir);
49 | if (fs.existsSync(rulesDir)) {
50 | const ruleFiles = Object.values(profile.fileMap);
51 | const hasRuleFiles = ruleFiles.some((ruleFile) =>
52 | fs.existsSync(path.join(rulesDir, ruleFile))
53 | );
54 | if (hasRuleFiles) {
55 | installedProfiles.push(profileName);
56 | }
57 | }
58 | }
59 | } catch (error) {
60 | // Skip profiles that can't be loaded
61 | }
62 | }
63 |
64 | return installedProfiles;
65 | }
66 |
67 | /**
68 | * Check if removing specified profiles would leave no profiles installed
69 | * @param {string} projectRoot - Project root directory
70 | * @param {string[]} profilesToRemove - Array of profile names to remove
71 | * @returns {boolean} - True if removal would leave no profiles
72 | */
73 | export function wouldRemovalLeaveNoProfiles(projectRoot, profilesToRemove) {
74 | const installedProfiles = getInstalledProfiles(projectRoot);
75 |
76 | // If no profiles are currently installed, removal cannot leave no profiles
77 | if (installedProfiles.length === 0) {
78 | return false;
79 | }
80 |
81 | const remainingProfiles = installedProfiles.filter(
82 | (profile) => !profilesToRemove.includes(profile)
83 | );
84 | return remainingProfiles.length === 0;
85 | }
86 |
87 | // =============================================================================
88 | // PROFILE SETUP
89 | // =============================================================================
90 |
91 | // Note: Profile choices are now generated dynamically within runInteractiveProfilesSetup()
92 | // to ensure proper alphabetical sorting and pagination configuration
93 |
94 | /**
95 | * Launches an interactive prompt for selecting which rule profiles to include in your project.
96 | *
97 | * This function dynamically lists all available profiles (from RULE_PROFILES) and presents them as checkboxes.
98 | * The user must select at least one profile (no defaults are pre-selected). The result is an array of selected profile names.
99 | *
100 | * Used by both project initialization (init) and the CLI 'task-master rules setup' command.
101 | *
102 | * @returns {Promise<string[]>} Array of selected profile names (e.g., ['cursor', 'windsurf'])
103 | */
104 | export async function runInteractiveProfilesSetup() {
105 | // Generate the profile list dynamically with proper display names, alphabetized
106 | const profileDescriptions = RULE_PROFILES.map((profileName) => {
107 | const displayName = getProfileDisplayName(profileName);
108 | const profile = getRulesProfile(profileName);
109 |
110 | // Determine description based on profile capabilities
111 | let description;
112 | const hasRules = Object.keys(profile.fileMap).length > 0;
113 | const hasMcpConfig = profile.mcpConfig === true;
114 |
115 | if (!profile.includeDefaultRules) {
116 | // Integration guide profiles (claude, codex, gemini, opencode, zed, amp) - don't include standard coding rules
117 | if (profileName === 'claude') {
118 | description = 'Integration guide with Task Master slash commands';
119 | } else if (profileName === 'codex') {
120 | description = 'Comprehensive Task Master integration guide';
121 | } else if (hasMcpConfig) {
122 | description = 'Integration guide and MCP config';
123 | } else {
124 | description = 'Integration guide';
125 | }
126 | } else if (hasRules && hasMcpConfig) {
127 | // Full rule profiles with MCP config
128 | if (profileName === 'roo') {
129 | description = 'Rule profile, MCP config, and agent modes';
130 | } else {
131 | description = 'Rule profile and MCP config';
132 | }
133 | } else if (hasRules) {
134 | // Rule profiles without MCP config
135 | description = 'Rule profile';
136 | }
137 |
138 | return {
139 | profileName,
140 | displayName,
141 | description
142 | };
143 | }).sort((a, b) => a.displayName.localeCompare(b.displayName));
144 |
145 | const profileListText = profileDescriptions
146 | .map(
147 | ({ displayName, description }) =>
148 | `${chalk.white('• ')}${chalk.yellow(displayName)}${chalk.white(` - ${description}`)}`
149 | )
150 | .join('\n');
151 |
152 | console.log(
153 | boxen(
154 | `${chalk.white.bold('Rule Profiles Setup')}\n\n${chalk.white(
155 | 'Rule profiles help enforce best practices and conventions for Task Master.\n' +
156 | 'Each profile provides coding guidelines tailored for specific AI coding environments.\n\n'
157 | )}${chalk.cyan('Available Profiles:')}\n${profileListText}`,
158 | {
159 | padding: 1,
160 | borderColor: 'blue',
161 | borderStyle: 'round',
162 | margin: { top: 1, bottom: 1 }
163 | }
164 | )
165 | );
166 |
167 | // Generate choices in the same order as the display text above
168 | const sortedChoices = profileDescriptions.map(
169 | ({ profileName, displayName }) => ({
170 | name: displayName,
171 | value: profileName
172 | })
173 | );
174 |
175 | const ruleProfilesQuestion = {
176 | type: 'checkbox',
177 | name: 'ruleProfiles',
178 | message: 'Which rule profiles would you like to add to your project?',
179 | choices: sortedChoices,
180 | pageSize: sortedChoices.length, // Show all options without pagination
181 | loop: false, // Disable loop scrolling
182 | validate: (input) => input.length > 0 || 'You must select at least one.'
183 | };
184 | const { ruleProfiles } = await inquirer.prompt([ruleProfilesQuestion]);
185 | return ruleProfiles;
186 | }
187 |
188 | // =============================================================================
189 | // PROFILE SUMMARY
190 | // =============================================================================
191 |
192 | /**
193 | * Generate appropriate summary message for a profile based on its type
194 | * @param {string} profileName - Name of the profile
195 | * @param {Object} addResult - Result object with success/failed counts
196 | * @returns {string} Formatted summary message
197 | */
198 | export function generateProfileSummary(profileName, addResult) {
199 | const profileConfig = getRulesProfile(profileName);
200 |
201 | if (!profileConfig.includeDefaultRules) {
202 | // Integration guide profiles (claude, codex, gemini, amp)
203 | return `Summary for ${profileName}: Integration guide installed.`;
204 | } else {
205 | // Rule profiles with coding guidelines
206 | return `Summary for ${profileName}: ${addResult.success} files processed, ${addResult.failed} failed.`;
207 | }
208 | }
209 |
210 | /**
211 | * Generate appropriate summary message for profile removal
212 | * @param {string} profileName - Name of the profile
213 | * @param {Object} removeResult - Result object from removal operation
214 | * @returns {string} Formatted summary message
215 | */
216 | export function generateProfileRemovalSummary(profileName, removeResult) {
217 | if (removeResult.skipped) {
218 | return `Summary for ${profileName}: Skipped (default or protected files)`;
219 | }
220 |
221 | if (removeResult.error && !removeResult.success) {
222 | return `Summary for ${profileName}: Failed to remove - ${removeResult.error}`;
223 | }
224 |
225 | const profileConfig = getRulesProfile(profileName);
226 |
227 | if (!profileConfig.includeDefaultRules) {
228 | // Integration guide profiles (claude, codex, gemini, amp)
229 | const baseMessage = `Summary for ${profileName}: Integration guide removed`;
230 | if (removeResult.notice) {
231 | return `${baseMessage} (${removeResult.notice})`;
232 | }
233 | return baseMessage;
234 | } else {
235 | // Rule profiles with coding guidelines
236 | const baseMessage = `Summary for ${profileName}: Rule profile removed`;
237 | if (removeResult.notice) {
238 | return `${baseMessage} (${removeResult.notice})`;
239 | }
240 | return baseMessage;
241 | }
242 | }
243 |
244 | /**
245 | * Categorize profiles and generate final summary statistics
246 | * @param {Array} addResults - Array of add result objects
247 | * @returns {Object} Object with categorized profiles and totals
248 | */
249 | export function categorizeProfileResults(addResults) {
250 | const successfulProfiles = [];
251 | let totalSuccess = 0;
252 | let totalFailed = 0;
253 |
254 | addResults.forEach((r) => {
255 | totalSuccess += r.success;
256 | totalFailed += r.failed;
257 |
258 | // All profiles are considered successful if they completed without major errors
259 | if (r.success > 0 || r.failed === 0) {
260 | successfulProfiles.push(r.profileName);
261 | }
262 | });
263 |
264 | return {
265 | successfulProfiles,
266 | allSuccessfulProfiles: successfulProfiles,
267 | totalSuccess,
268 | totalFailed
269 | };
270 | }
271 |
272 | /**
273 | * Categorize removal results and generate final summary statistics
274 | * @param {Array} removalResults - Array of removal result objects
275 | * @returns {Object} Object with categorized removal results
276 | */
277 | export function categorizeRemovalResults(removalResults) {
278 | const successfulRemovals = [];
279 | const skippedRemovals = [];
280 | const failedRemovals = [];
281 | const removalsWithNotices = [];
282 |
283 | removalResults.forEach((result) => {
284 | if (result.success) {
285 | successfulRemovals.push(result.profileName);
286 | } else if (result.skipped) {
287 | skippedRemovals.push(result.profileName);
288 | } else if (result.error) {
289 | failedRemovals.push(result);
290 | }
291 |
292 | if (result.notice) {
293 | removalsWithNotices.push(result);
294 | }
295 | });
296 |
297 | return {
298 | successfulRemovals,
299 | skippedRemovals,
300 | failedRemovals,
301 | removalsWithNotices
302 | };
303 | }
304 |
```
--------------------------------------------------------------------------------
/tests/e2e/e2e_helpers.sh:
--------------------------------------------------------------------------------
```bash
1 | #!/bin/bash
2 |
3 | # --- LLM Analysis Helper Function ---
4 | # This function should be sourced by the main E2E script or test scripts.
5 | # It requires curl and jq to be installed.
6 | # It expects the project root path to be passed as the second argument.
7 |
8 | # --- New Function: extract_and_sum_cost ---
9 | # Takes a string containing command output.
10 | # Extracts costs (lines with "Est. Cost: $X.YYYYYY" or similar from telemetry output)
11 | # from the output, sums them, and adds them to the GLOBAL total_e2e_cost variable.
12 | extract_and_sum_cost() {
13 | local command_output="$1"
14 | # Ensure total_e2e_cost is treated as a number, default to 0.0 if not set or invalid
15 | if ! [[ "$total_e2e_cost" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
16 | total_e2e_cost="0.0"
17 | fi
18 |
19 | local extracted_cost_sum="0.0"
20 |
21 | # Grep for lines containing "Est. Cost: $", then extract the numeric value.
22 | # Example line: │ Est. Cost: $0.093549 │
23 | # Accumulate all costs found in the command_output
24 | while IFS= read -r line; do
25 | # Extract the numeric part after 'Est. Cost: $' and before any trailing spaces/chars
26 | cost_value=$(echo "$line" | grep -o -E 'Est\. Cost: \$([0-9]+\.[0-9]+)' | sed -E 's/Est\. Cost: \$//g')
27 | if [[ -n "$cost_value" && "$cost_value" =~ ^[0-9]+\.[0-9]+$ ]]; then
28 | # echo "[DEBUG] Found cost value: $cost_value in line: '$line'" # For debugging
29 | extracted_cost_sum=$(echo "$extracted_cost_sum + $cost_value" | bc)
30 | # else # For debugging
31 | # echo "[DEBUG] No valid cost value found or extracted in line: '$line' (extracted: '$cost_value')" # For debugging
32 | fi
33 | done < <(echo "$command_output" | grep -E 'Est\. Cost: \$')
34 |
35 | # echo "[DEBUG] Extracted sum from this command output: $extracted_cost_sum" # For debugging
36 | if (( $(echo "$extracted_cost_sum > 0" | bc -l) )); then
37 | total_e2e_cost=$(echo "$total_e2e_cost + $extracted_cost_sum" | bc)
38 | # echo "[DEBUG] Updated global total_e2e_cost: $total_e2e_cost" # For debugging
39 | fi
40 | # No echo here, the function modifies a global variable.
41 | }
42 | export -f extract_and_sum_cost # Export for use in other scripts if sourced
43 |
44 | analyze_log_with_llm() {
45 | local log_file="$1"
46 | local project_root="$2" # Expect project root as the second argument
47 |
48 | if [ -z "$project_root" ]; then
49 | echo "[HELPER_ERROR] Project root argument is missing. Skipping LLM analysis." >&2
50 | return 1
51 | fi
52 |
53 | local env_file="${project_root}/.env" # Path to .env in project root
54 | local supported_models_file="${project_root}/scripts/modules/supported-models.json"
55 |
56 | local provider_summary_log="provider_add_task_summary.log" # File summarizing provider test outcomes
57 | local api_key=""
58 | local api_endpoint="https://api.anthropic.com/v1/messages"
59 | local api_key_name="ANTHROPIC_API_KEY"
60 | local llm_analysis_model_id="claude-3-7-sonnet-20250219" # Model used for this analysis
61 | local llm_analysis_provider="anthropic"
62 |
63 | echo "" # Add a newline before analysis starts
64 |
65 | if ! command -v jq &> /dev/null; then
66 | echo "[HELPER_ERROR] LLM Analysis requires 'jq'. Skipping analysis." >&2
67 | return 1
68 | fi
69 | if ! command -v curl &> /dev/null; then
70 | echo "[HELPER_ERROR] LLM Analysis requires 'curl'. Skipping analysis." >&2
71 | return 1
72 | fi
73 | if ! command -v bc &> /dev/null; then
74 | echo "[HELPER_ERROR] LLM Analysis requires 'bc' for cost calculation. Skipping analysis." >&2
75 | return 1
76 | fi
77 |
78 | if [ -f "$env_file" ]; then
79 | api_key=$(grep "^${api_key_name}=" "$env_file" | sed -e "s/^${api_key_name}=//" -e 's/^[[:space:]"]*//' -e 's/[[:space:]"]*$//')
80 | fi
81 |
82 | if [ -z "$api_key" ]; then
83 | echo "[HELPER_ERROR] ${api_key_name} not found or empty in project root .env file ($env_file). Skipping LLM analysis." >&2
84 | return 1
85 | fi
86 |
87 | if [ ! -f "$log_file" ]; then
88 | echo "[HELPER_ERROR] Log file not found: $log_file (PWD: $(pwd)). Check path passed to function. Skipping LLM analysis." >&2
89 | return 1
90 | fi
91 |
92 | local log_content
93 | log_content=$(cat "$log_file") || {
94 | echo "[HELPER_ERROR] Failed to read log file: $log_file. Skipping LLM analysis." >&2
95 | return 1
96 | }
97 |
98 | read -r -d '' prompt_template <<'EOF'
99 | Analyze the following E2E test log for the task-master tool. The log contains output from various 'task-master' commands executed sequentially.
100 |
101 | Your goal is to:
102 | 1. Verify if the key E2E steps completed successfully based on the log messages (e.g., init, parse PRD, list tasks, analyze complexity, expand task, set status, manage models, add/remove dependencies, add/update/remove tasks/subtasks, generate files).
103 | 2. **Specifically analyze the Multi-Provider Add-Task Test Sequence:**
104 | a. Identify which providers were tested for `add-task`. Look for log steps like "Testing Add-Task with Provider: ..." and the summary log 'provider_add_task_summary.log'.
105 | b. For each tested provider, determine if `add-task` succeeded or failed. Note the created task ID if successful.
106 | c. Review the corresponding `add_task_show_output_<provider>_id_<id>.log` file (if created) for each successful `add-task` execution.
107 | d. **Compare the quality and completeness** of the task generated by each successful provider based on their `show` output. Assign a score (e.g., 1-10, 10 being best) based on relevance to the prompt, detail level, and correctness.
108 | e. Note any providers where `add-task` failed or where the task ID could not be extracted.
109 | 3. Identify any general explicit "[ERROR]" messages or stack traces throughout the *entire* log.
110 | 4. Identify any potential warnings or unusual output that might indicate a problem even if not marked as an explicit error.
111 | 5. Provide an overall assessment of the test run's health based *only* on the log content.
112 |
113 | Return your analysis **strictly** in the following JSON format. Do not include any text outside of the JSON structure:
114 |
115 | {
116 | "overall_status": "Success|Failure|Warning",
117 | "verified_steps": [ "Initialization", "PRD Parsing", /* ...other general steps observed... */ ],
118 | "provider_add_task_comparison": {
119 | "prompt_used": "... (extract from log if possible or state 'standard auth prompt') ...",
120 | "provider_results": {
121 | "anthropic": { "status": "Success|Failure|ID_Extraction_Failed|Set_Model_Failed", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },
122 | "openai": { "status": "Success|Failure|...", "task_id": "...", "score": "X/10 | N/A", "notes": "..." },
123 | /* ... include all tested providers ... */
124 | },
125 | "comparison_summary": "Brief overall comparison of generated tasks..."
126 | },
127 | "detected_issues": [ { "severity": "Error|Warning|Anomaly", "description": "...", "log_context": "[Optional, short snippet from log near the issue]" } ],
128 | "llm_summary_points": [ "Overall summary point 1", "Provider comparison highlight", "Any major issues noted" ]
129 | }
130 |
131 | Here is the main log content:
132 |
133 | %s
134 | EOF
135 |
136 | local full_prompt
137 | if ! printf -v full_prompt "$prompt_template" "$log_content"; then
138 | echo "[HELPER_ERROR] Failed to format prompt using printf." >&2
139 | return 1
140 | fi
141 |
142 | local payload
143 | payload=$(jq -n --arg prompt "$full_prompt" '{
144 | "model": "'"$llm_analysis_model_id"'",
145 | "max_tokens": 3072,
146 | "messages": [
147 | {"role": "user", "content": $prompt}
148 | ]
149 | }') || {
150 | echo "[HELPER_ERROR] Failed to create JSON payload using jq." >&2
151 | return 1
152 | }
153 |
154 | local response_raw response_http_code response_body
155 | response_raw=$(curl -s -w "\nHTTP_STATUS_CODE:%{http_code}" -X POST "$api_endpoint" \
156 | -H "Content-Type: application/json" \
157 | -H "x-api-key: $api_key" \
158 | -H "anthropic-version: 2023-06-01" \
159 | --data "$payload")
160 |
161 | response_http_code=$(echo "$response_raw" | grep '^HTTP_STATUS_CODE:' | sed 's/HTTP_STATUS_CODE://')
162 | response_body=$(echo "$response_raw" | sed '$d')
163 |
164 | if [ "$response_http_code" != "200" ]; then
165 | echo "[HELPER_ERROR] LLM API call failed with HTTP status $response_http_code." >&2
166 | echo "[HELPER_ERROR] Response Body: $response_body" >&2
167 | return 1
168 | fi
169 |
170 | if [ -z "$response_body" ]; then
171 | echo "[HELPER_ERROR] LLM API call returned empty response body." >&2
172 | return 1
173 | fi
174 |
175 | # Calculate cost of this LLM analysis call
176 | local input_tokens output_tokens input_cost_per_1m output_cost_per_1m calculated_llm_cost
177 | input_tokens=$(echo "$response_body" | jq -r '.usage.input_tokens // 0')
178 | output_tokens=$(echo "$response_body" | jq -r '.usage.output_tokens // 0')
179 |
180 | if [ -f "$supported_models_file" ]; then
181 | model_cost_info=$(jq -r --arg provider "$llm_analysis_provider" --arg model_id "$llm_analysis_model_id" '
182 | .[$provider][] | select(.id == $model_id) | .cost_per_1m_tokens
183 | ' "$supported_models_file")
184 |
185 | if [[ -n "$model_cost_info" && "$model_cost_info" != "null" ]]; then
186 | input_cost_per_1m=$(echo "$model_cost_info" | jq -r '.input // 0')
187 | output_cost_per_1m=$(echo "$model_cost_info" | jq -r '.output // 0')
188 |
189 | calculated_llm_cost=$(echo "($input_tokens / 1000000 * $input_cost_per_1m) + ($output_tokens / 1000000 * $output_cost_per_1m)" | bc -l)
190 | # Format to 6 decimal places
191 | formatted_llm_cost=$(printf "%.6f" "$calculated_llm_cost")
192 | echo "LLM Analysis AI Cost: $formatted_llm_cost USD" # This line will be parsed by run_e2e.sh
193 | else
194 | echo "[HELPER_WARNING] Cost data for model $llm_analysis_model_id not found in $supported_models_file. LLM analysis cost not calculated."
195 | fi
196 | else
197 | echo "[HELPER_WARNING] $supported_models_file not found. LLM analysis cost not calculated."
198 | fi
199 | # --- End cost calculation for this call ---
200 |
201 | if echo "$response_body" | node "${project_root}/tests/e2e/parse_llm_output.cjs" "$log_file"; then
202 | echo "[HELPER_SUCCESS] LLM analysis parsed and printed successfully by Node.js script."
203 | return 0
204 | else
205 | local node_exit_code=$?
206 | echo "[HELPER_ERROR] Node.js parsing script failed with exit code ${node_exit_code}."
207 | echo "[HELPER_ERROR] Raw API response body (first 500 chars): $(echo "$response_body" | head -c 500)"
208 | return 1
209 | fi
210 | }
211 |
212 | export -f analyze_log_with_llm
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/utils/git-utils.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Git utilities for Task Master
3 | * Git integration utilities using raw git commands and gh CLI
4 | */
5 |
6 | import { exec, execSync } from 'child_process';
7 | import { promisify } from 'util';
8 |
9 | const execAsync = promisify(exec);
10 |
11 | /**
12 | * GitHub repository information
13 | */
14 | export interface GitHubRepoInfo {
15 | name: string;
16 | owner: { login: string };
17 | defaultBranchRef: { name: string };
18 | }
19 |
20 | /**
21 | * Check if the specified directory is inside a git repository
22 | */
23 | export async function isGitRepository(projectRoot: string): Promise<boolean> {
24 | if (!projectRoot) {
25 | throw new Error('projectRoot is required for isGitRepository');
26 | }
27 |
28 | try {
29 | await execAsync('git rev-parse --git-dir', { cwd: projectRoot });
30 | return true;
31 | } catch (error) {
32 | return false;
33 | }
34 | }
35 |
36 | /**
37 | * Synchronous check if directory is in a git repository
38 | */
39 | export function isGitRepositorySync(projectRoot: string): boolean {
40 | if (!projectRoot) {
41 | return false;
42 | }
43 |
44 | try {
45 | execSync('git rev-parse --git-dir', {
46 | cwd: projectRoot,
47 | stdio: 'ignore'
48 | });
49 | return true;
50 | } catch (error) {
51 | return false;
52 | }
53 | }
54 |
55 | /**
56 | * Get the current git branch name
57 | */
58 | export async function getCurrentBranch(
59 | projectRoot: string
60 | ): Promise<string | null> {
61 | if (!projectRoot) {
62 | throw new Error('projectRoot is required for getCurrentBranch');
63 | }
64 |
65 | try {
66 | const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
67 | cwd: projectRoot
68 | });
69 | return stdout.trim();
70 | } catch (error) {
71 | return null;
72 | }
73 | }
74 |
75 | /**
76 | * Synchronous get current git branch name
77 | */
78 | export function getCurrentBranchSync(projectRoot: string): string | null {
79 | if (!projectRoot) {
80 | return null;
81 | }
82 |
83 | try {
84 | const stdout = execSync('git rev-parse --abbrev-ref HEAD', {
85 | cwd: projectRoot,
86 | encoding: 'utf8'
87 | });
88 | return stdout.trim();
89 | } catch (error) {
90 | return null;
91 | }
92 | }
93 |
94 | /**
95 | * Get list of all local git branches
96 | */
97 | export async function getLocalBranches(projectRoot: string): Promise<string[]> {
98 | if (!projectRoot) {
99 | throw new Error('projectRoot is required for getLocalBranches');
100 | }
101 |
102 | try {
103 | const { stdout } = await execAsync(
104 | 'git branch --format="%(refname:short)"',
105 | { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 }
106 | );
107 | return stdout
108 | .trim()
109 | .split('\n')
110 | .filter((branch) => branch.length > 0)
111 | .map((branch) => branch.trim());
112 | } catch (error) {
113 | return [];
114 | }
115 | }
116 |
117 | /**
118 | * Get list of all remote branches
119 | */
120 | export async function getRemoteBranches(
121 | projectRoot: string
122 | ): Promise<string[]> {
123 | if (!projectRoot) {
124 | throw new Error('projectRoot is required for getRemoteBranches');
125 | }
126 |
127 | try {
128 | const { stdout } = await execAsync(
129 | 'git branch -r --format="%(refname:short)"',
130 | { cwd: projectRoot, maxBuffer: 10 * 1024 * 1024 }
131 | );
132 | const names = stdout
133 | .trim()
134 | .split('\n')
135 | .filter((branch) => branch.length > 0 && !branch.includes('HEAD'))
136 | .map((branch) => branch.replace(/^[^/]+\//, '').trim());
137 | return Array.from(new Set(names));
138 | } catch (error) {
139 | return [];
140 | }
141 | }
142 |
143 | /**
144 | * Check if gh CLI is available and authenticated
145 | */
146 | export async function isGhCliAvailable(projectRoot?: string): Promise<boolean> {
147 | try {
148 | const options = projectRoot ? { cwd: projectRoot } : {};
149 | await execAsync('gh auth status', options);
150 | return true;
151 | } catch (error) {
152 | return false;
153 | }
154 | }
155 |
156 | /**
157 | * Get GitHub repository information using gh CLI
158 | */
159 | export async function getGitHubRepoInfo(
160 | projectRoot: string
161 | ): Promise<GitHubRepoInfo | null> {
162 | if (!projectRoot) {
163 | throw new Error('projectRoot is required for getGitHubRepoInfo');
164 | }
165 |
166 | try {
167 | const { stdout } = await execAsync(
168 | 'gh repo view --json name,owner,defaultBranchRef',
169 | { cwd: projectRoot }
170 | );
171 | return JSON.parse(stdout) as GitHubRepoInfo;
172 | } catch (error) {
173 | return null;
174 | }
175 | }
176 |
177 | /**
178 | * Get git repository root directory
179 | */
180 | export async function getGitRepositoryRoot(
181 | projectRoot: string
182 | ): Promise<string | null> {
183 | if (!projectRoot) {
184 | throw new Error('projectRoot is required for getGitRepositoryRoot');
185 | }
186 |
187 | try {
188 | const { stdout } = await execAsync('git rev-parse --show-toplevel', {
189 | cwd: projectRoot
190 | });
191 | return stdout.trim();
192 | } catch (error) {
193 | return null;
194 | }
195 | }
196 |
197 | /**
198 | * Get the default branch name for the repository
199 | */
200 | export async function getDefaultBranch(
201 | projectRoot: string
202 | ): Promise<string | null> {
203 | if (!projectRoot) {
204 | throw new Error('projectRoot is required for getDefaultBranch');
205 | }
206 |
207 | try {
208 | // Try to get from GitHub first (if gh CLI is available)
209 | if (await isGhCliAvailable(projectRoot)) {
210 | const repoInfo = await getGitHubRepoInfo(projectRoot);
211 | if (repoInfo && repoInfo.defaultBranchRef) {
212 | return repoInfo.defaultBranchRef.name;
213 | }
214 | }
215 |
216 | // Fallback to git remote info (support non-origin remotes)
217 | const remotesRaw = await execAsync('git remote', { cwd: projectRoot });
218 | const remotes = remotesRaw.stdout.trim().split('\n').filter(Boolean);
219 | if (remotes.length > 0) {
220 | const primary = remotes.includes('origin') ? 'origin' : remotes[0];
221 | // Parse `git remote show` (preferred)
222 | try {
223 | const { stdout } = await execAsync(`git remote show ${primary}`, {
224 | cwd: projectRoot,
225 | maxBuffer: 10 * 1024 * 1024
226 | });
227 | const m = stdout.match(/HEAD branch:\s+([^\s]+)/);
228 | if (m) return m[1].trim();
229 | } catch {}
230 | // Fallback to symbolic-ref of remote HEAD
231 | try {
232 | const { stdout } = await execAsync(
233 | `git symbolic-ref refs/remotes/${primary}/HEAD`,
234 | { cwd: projectRoot }
235 | );
236 | return stdout.replace(`refs/remotes/${primary}/`, '').trim();
237 | } catch {}
238 | }
239 | // If we couldn't determine, throw to trigger final fallbacks
240 | throw new Error('default-branch-not-found');
241 | } catch (error) {
242 | // Final fallback - common default branch names
243 | const commonDefaults = ['main', 'master'];
244 | const branches = await getLocalBranches(projectRoot);
245 | const remoteBranches = await getRemoteBranches(projectRoot);
246 |
247 | for (const defaultName of commonDefaults) {
248 | if (
249 | branches.includes(defaultName) ||
250 | remoteBranches.includes(defaultName)
251 | ) {
252 | return defaultName;
253 | }
254 | }
255 |
256 | return null;
257 | }
258 | }
259 |
260 | /**
261 | * Check if we're currently on the default branch
262 | */
263 | export async function isOnDefaultBranch(projectRoot: string): Promise<boolean> {
264 | if (!projectRoot) {
265 | throw new Error('projectRoot is required for isOnDefaultBranch');
266 | }
267 |
268 | try {
269 | const [currentBranch, defaultBranch] = await Promise.all([
270 | getCurrentBranch(projectRoot),
271 | getDefaultBranch(projectRoot)
272 | ]);
273 | return (
274 | currentBranch !== null &&
275 | defaultBranch !== null &&
276 | currentBranch === defaultBranch
277 | );
278 | } catch (error) {
279 | return false;
280 | }
281 | }
282 |
283 | /**
284 | * Check if the current working directory is inside a Git work-tree
285 | */
286 | export function insideGitWorkTree(): boolean {
287 | try {
288 | execSync('git rev-parse --is-inside-work-tree', {
289 | stdio: 'ignore',
290 | cwd: process.cwd()
291 | });
292 | return true;
293 | } catch {
294 | return false;
295 | }
296 | }
297 |
298 | /**
299 | * Sanitize branch name to be a valid tag name
300 | */
301 | export function sanitizeBranchNameForTag(branchName: string): string {
302 | if (!branchName || typeof branchName !== 'string') {
303 | return 'unknown-branch';
304 | }
305 |
306 | // Replace invalid characters with hyphens and clean up
307 | return branchName
308 | .replace(/[^a-zA-Z0-9_.-]/g, '-') // Replace invalid chars with hyphens (allow dots)
309 | .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
310 | .replace(/-+/g, '-') // Collapse multiple hyphens
311 | .toLowerCase() // Convert to lowercase
312 | .substring(0, 50); // Limit length
313 | }
314 |
315 | /**
316 | * Check if a branch name would create a valid tag name
317 | */
318 | export function isValidBranchForTag(branchName: string): boolean {
319 | if (!branchName || typeof branchName !== 'string') {
320 | return false;
321 | }
322 |
323 | // Check if it's a reserved branch name that shouldn't become tags
324 | const reservedBranches = ['main', 'master', 'develop', 'dev', 'head'];
325 | if (reservedBranches.includes(branchName.toLowerCase())) {
326 | return false;
327 | }
328 |
329 | // Check if sanitized name would be meaningful
330 | const sanitized = sanitizeBranchNameForTag(branchName);
331 | return sanitized.length > 0 && sanitized !== 'unknown-branch';
332 | }
333 |
334 | /**
335 | * Git worktree information
336 | */
337 | export interface GitWorktree {
338 | path: string;
339 | branch: string | null;
340 | head: string;
341 | }
342 |
343 | /**
344 | * Get list of all git worktrees
345 | */
346 | export async function getWorktrees(
347 | projectRoot: string
348 | ): Promise<GitWorktree[]> {
349 | if (!projectRoot) {
350 | throw new Error('projectRoot is required for getWorktrees');
351 | }
352 |
353 | try {
354 | const { stdout } = await execAsync('git worktree list --porcelain', {
355 | cwd: projectRoot
356 | });
357 |
358 | const worktrees: GitWorktree[] = [];
359 | const lines = stdout.trim().split('\n');
360 | let current: Partial<GitWorktree> = {};
361 |
362 | for (const line of lines) {
363 | if (line.startsWith('worktree ')) {
364 | // flush previous entry if present
365 | if (current.path) {
366 | worktrees.push({
367 | path: current.path,
368 | branch: current.branch || null,
369 | head: current.head || ''
370 | });
371 | current = {};
372 | }
373 | current.path = line.substring(9);
374 | } else if (line.startsWith('HEAD ')) {
375 | current.head = line.substring(5);
376 | } else if (line.startsWith('branch ')) {
377 | current.branch = line.substring(7).replace('refs/heads/', '');
378 | } else if (line === '' && current.path) {
379 | worktrees.push({
380 | path: current.path,
381 | branch: current.branch || null,
382 | head: current.head || ''
383 | });
384 | current = {};
385 | }
386 | }
387 |
388 | // Handle last entry if no trailing newline
389 | if (current.path) {
390 | worktrees.push({
391 | path: current.path,
392 | branch: current.branch || null,
393 | head: current.head || ''
394 | });
395 | }
396 |
397 | return worktrees;
398 | } catch (error) {
399 | return [];
400 | }
401 | }
402 |
403 | /**
404 | * Check if a branch is checked out in any worktree
405 | * Returns the worktree path if found, null otherwise
406 | */
407 | export async function isBranchCheckedOut(
408 | projectRoot: string,
409 | branchName: string
410 | ): Promise<string | null> {
411 | if (!projectRoot) {
412 | throw new Error('projectRoot is required for isBranchCheckedOut');
413 | }
414 | if (!branchName) {
415 | throw new Error('branchName is required for isBranchCheckedOut');
416 | }
417 |
418 | const worktrees = await getWorktrees(projectRoot);
419 | const worktree = worktrees.find((wt) => wt.branch === branchName);
420 | return worktree ? worktree.path : null;
421 | }
422 |
```
--------------------------------------------------------------------------------
/scripts/modules/task-manager/parse-prd/parse-prd-helpers.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Helper functions for PRD parsing
3 | */
4 |
5 | import fs from 'fs';
6 | import path from 'path';
7 | import boxen from 'boxen';
8 | import chalk from 'chalk';
9 | import { ensureTagMetadata, findTaskById } from '../../utils.js';
10 | import { displayParsePrdSummary } from '../../../../src/ui/parse-prd.js';
11 | import { TimeoutManager } from '../../../../src/utils/timeout-manager.js';
12 | import { displayAiUsageSummary } from '../../ui.js';
13 | import { getPromptManager } from '../../prompt-manager.js';
14 | import { getDefaultPriority } from '../../config-manager.js';
15 |
16 | /**
17 | * Estimate token count from text
18 | * @param {string} text - Text to estimate tokens for
19 | * @returns {number} Estimated token count
20 | */
21 | export function estimateTokens(text) {
22 | // Common approximation: ~4 characters per token for English
23 | return Math.ceil(text.length / 4);
24 | }
25 |
26 | /**
27 | * Read and validate PRD content
28 | * @param {string} prdPath - Path to PRD file
29 | * @returns {string} PRD content
30 | * @throws {Error} If file is empty or cannot be read
31 | */
32 | export function readPrdContent(prdPath) {
33 | const prdContent = fs.readFileSync(prdPath, 'utf8');
34 | if (!prdContent) {
35 | throw new Error(`Input file ${prdPath} is empty or could not be read.`);
36 | }
37 | return prdContent;
38 | }
39 |
40 | /**
41 | * Load existing tasks from file
42 | * @param {string} tasksPath - Path to tasks file
43 | * @param {string} targetTag - Target tag to load from
44 | * @returns {{tasks: Array, nextId: number}} Existing tasks and next ID
45 | */
46 | export function loadExistingTasks(tasksPath, targetTag) {
47 | let existingTasks = [];
48 | let nextId = 1;
49 |
50 | if (!fs.existsSync(tasksPath)) {
51 | return { existingTasks, nextId };
52 | }
53 |
54 | try {
55 | const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
56 | const allData = JSON.parse(existingFileContent);
57 |
58 | if (allData[targetTag]?.tasks && Array.isArray(allData[targetTag].tasks)) {
59 | existingTasks = allData[targetTag].tasks;
60 | if (existingTasks.length > 0) {
61 | nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
62 | }
63 | }
64 | } catch (error) {
65 | // If we can't read the file or parse it, assume no existing tasks
66 | return { existingTasks: [], nextId: 1 };
67 | }
68 |
69 | return { existingTasks, nextId };
70 | }
71 |
72 | /**
73 | * Validate overwrite/append operations
74 | * @param {Object} params
75 | * @returns {void}
76 | * @throws {Error} If validation fails
77 | */
78 | export function validateFileOperations({
79 | existingTasks,
80 | targetTag,
81 | append,
82 | force,
83 | isMCP,
84 | logger
85 | }) {
86 | const hasExistingTasks = existingTasks.length > 0;
87 |
88 | if (!hasExistingTasks) {
89 | logger.report(
90 | `Tag '${targetTag}' is empty or doesn't exist. Creating/updating tag with new tasks.`,
91 | 'info'
92 | );
93 | return;
94 | }
95 |
96 | if (append) {
97 | logger.report(
98 | `Append mode enabled. Found ${existingTasks.length} existing tasks in tag '${targetTag}'.`,
99 | 'info'
100 | );
101 | return;
102 | }
103 |
104 | if (!force) {
105 | const errorMessage = `Tag '${targetTag}' already contains ${existingTasks.length} tasks. Use --force to overwrite or --append to add to existing tasks.`;
106 | logger.report(errorMessage, 'error');
107 |
108 | if (isMCP) {
109 | throw new Error(errorMessage);
110 | } else {
111 | console.error(chalk.red(errorMessage));
112 | process.exit(1);
113 | }
114 | }
115 |
116 | logger.report(
117 | `Force flag enabled. Overwriting existing tasks in tag '${targetTag}'.`,
118 | 'debug'
119 | );
120 | }
121 |
122 | /**
123 | * Process and transform tasks with ID remapping
124 | * @param {Array} rawTasks - Raw tasks from AI
125 | * @param {number} startId - Starting ID for new tasks
126 | * @param {Array} existingTasks - Existing tasks for dependency validation
127 | * @param {string} defaultPriority - Default priority for tasks
128 | * @returns {Array} Processed tasks with remapped IDs
129 | */
130 | export function processTasks(
131 | rawTasks,
132 | startId,
133 | existingTasks,
134 | defaultPriority
135 | ) {
136 | let currentId = startId;
137 | const taskMap = new Map();
138 |
139 | // First pass: assign new IDs and create mapping
140 | const processedTasks = rawTasks.map((task) => {
141 | const newId = currentId++;
142 | taskMap.set(task.id, newId);
143 |
144 | return {
145 | ...task,
146 | id: newId,
147 | status: task.status || 'pending',
148 | priority: task.priority || defaultPriority,
149 | dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
150 | subtasks: task.subtasks || [],
151 | // Ensure all required fields have values
152 | title: task.title || '',
153 | description: task.description || '',
154 | details: task.details || '',
155 | testStrategy: task.testStrategy || ''
156 | };
157 | });
158 |
159 | // Second pass: remap dependencies
160 | processedTasks.forEach((task) => {
161 | task.dependencies = task.dependencies
162 | .map((depId) => taskMap.get(depId))
163 | .filter(
164 | (newDepId) =>
165 | newDepId != null &&
166 | newDepId < task.id &&
167 | (findTaskById(existingTasks, newDepId) ||
168 | processedTasks.some((t) => t.id === newDepId))
169 | );
170 | });
171 |
172 | return processedTasks;
173 | }
174 |
175 | /**
176 | * Save tasks to file with tag support
177 | * @param {string} tasksPath - Path to save tasks
178 | * @param {Array} tasks - Tasks to save
179 | * @param {string} targetTag - Target tag
180 | * @param {Object} logger - Logger instance
181 | */
182 | export function saveTasksToFile(tasksPath, tasks, targetTag, logger) {
183 | // Create directory if it doesn't exist
184 | const tasksDir = path.dirname(tasksPath);
185 | if (!fs.existsSync(tasksDir)) {
186 | fs.mkdirSync(tasksDir, { recursive: true });
187 | }
188 |
189 | // Read existing file to preserve other tags
190 | let outputData = {};
191 | if (fs.existsSync(tasksPath)) {
192 | try {
193 | const existingFileContent = fs.readFileSync(tasksPath, 'utf8');
194 | outputData = JSON.parse(existingFileContent);
195 | } catch (error) {
196 | outputData = {};
197 | }
198 | }
199 |
200 | // Update only the target tag
201 | outputData[targetTag] = {
202 | tasks: tasks,
203 | metadata: {
204 | created:
205 | outputData[targetTag]?.metadata?.created || new Date().toISOString(),
206 | updated: new Date().toISOString(),
207 | description: `Tasks for ${targetTag} context`
208 | }
209 | };
210 |
211 | // Ensure proper metadata
212 | ensureTagMetadata(outputData[targetTag], {
213 | description: `Tasks for ${targetTag} context`
214 | });
215 |
216 | // Write back to file
217 | fs.writeFileSync(tasksPath, JSON.stringify(outputData, null, 2));
218 |
219 | logger.report(
220 | `Successfully saved ${tasks.length} tasks to ${tasksPath}`,
221 | 'debug'
222 | );
223 | }
224 |
225 | /**
226 | * Build prompts for AI service
227 | * @param {Object} config - Configuration object
228 | * @param {string} prdContent - PRD content
229 | * @param {number} nextId - Next task ID
230 | * @returns {Promise<{systemPrompt: string, userPrompt: string}>}
231 | */
232 | export async function buildPrompts(config, prdContent, nextId) {
233 | const promptManager = getPromptManager();
234 | const defaultTaskPriority =
235 | getDefaultPriority(config.projectRoot) || 'medium';
236 |
237 | return promptManager.loadPrompt('parse-prd', {
238 | research: config.research,
239 | numTasks: config.numTasks,
240 | nextId,
241 | prdContent,
242 | prdPath: config.prdPath,
243 | defaultTaskPriority,
244 | hasCodebaseAnalysis: config.hasCodebaseAnalysis(),
245 | projectRoot: config.projectRoot || ''
246 | });
247 | }
248 |
249 | /**
250 | * Handle progress reporting for both CLI and MCP
251 | * @param {Object} params
252 | */
253 | export async function reportTaskProgress({
254 | task,
255 | currentCount,
256 | totalTasks,
257 | estimatedTokens,
258 | progressTracker,
259 | reportProgress,
260 | priorityMap,
261 | defaultPriority,
262 | estimatedInputTokens
263 | }) {
264 | const priority = task.priority || defaultPriority;
265 | const priorityIndicator = priorityMap[priority] || priorityMap.medium;
266 |
267 | // CLI progress tracker
268 | if (progressTracker) {
269 | progressTracker.addTaskLine(currentCount, task.title, priority);
270 | if (estimatedTokens) {
271 | progressTracker.updateTokens(estimatedInputTokens, estimatedTokens);
272 | }
273 | }
274 |
275 | // MCP progress reporting
276 | if (reportProgress) {
277 | try {
278 | const outputTokens = estimatedTokens
279 | ? Math.floor(estimatedTokens / totalTasks)
280 | : 0;
281 |
282 | await reportProgress({
283 | progress: currentCount,
284 | total: totalTasks,
285 | message: `${priorityIndicator} Task ${currentCount}/${totalTasks} - ${task.title} | ~Output: ${outputTokens} tokens`
286 | });
287 | } catch (error) {
288 | // Ignore progress reporting errors
289 | }
290 | }
291 | }
292 |
293 | /**
294 | * Display completion summary for CLI
295 | * @param {Object} params
296 | */
297 | export async function displayCliSummary({
298 | processedTasks,
299 | nextId,
300 | summary,
301 | prdPath,
302 | tasksPath,
303 | usedFallback,
304 | aiServiceResponse
305 | }) {
306 | // Generate task file names
307 | const taskFilesGenerated = (() => {
308 | if (!Array.isArray(processedTasks) || processedTasks.length === 0) {
309 | return `task_${String(nextId).padStart(3, '0')}.txt`;
310 | }
311 | const firstNewTaskId = processedTasks[0].id;
312 | const lastNewTaskId = processedTasks[processedTasks.length - 1].id;
313 | if (processedTasks.length === 1) {
314 | return `task_${String(firstNewTaskId).padStart(3, '0')}.txt`;
315 | }
316 | return `task_${String(firstNewTaskId).padStart(3, '0')}.txt -> task_${String(lastNewTaskId).padStart(3, '0')}.txt`;
317 | })();
318 |
319 | displayParsePrdSummary({
320 | totalTasks: processedTasks.length,
321 | taskPriorities: summary.taskPriorities,
322 | prdFilePath: prdPath,
323 | outputPath: tasksPath,
324 | elapsedTime: summary.elapsedTime,
325 | usedFallback,
326 | taskFilesGenerated,
327 | actionVerb: summary.actionVerb
328 | });
329 |
330 | // Display telemetry
331 | if (aiServiceResponse?.telemetryData) {
332 | // For streaming, wait briefly to allow usage data to be captured
333 | if (aiServiceResponse.mainResult?.usage) {
334 | // Give the usage promise a short time to resolve
335 | await TimeoutManager.withSoftTimeout(
336 | aiServiceResponse.mainResult.usage,
337 | 1000,
338 | undefined
339 | );
340 | }
341 | displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
342 | }
343 | }
344 |
345 | /**
346 | * Display non-streaming CLI output
347 | * @param {Object} params
348 | */
349 | export function displayNonStreamingCliOutput({
350 | processedTasks,
351 | research,
352 | finalTasks,
353 | tasksPath,
354 | aiServiceResponse
355 | }) {
356 | console.log(
357 | boxen(
358 | chalk.green(
359 | `Successfully generated ${processedTasks.length} new tasks${research ? ' with research-backed analysis' : ''}. Total tasks in ${tasksPath}: ${finalTasks.length}`
360 | ),
361 | { padding: 1, borderColor: 'green', borderStyle: 'round' }
362 | )
363 | );
364 |
365 | console.log(
366 | boxen(
367 | chalk.white.bold('Next Steps:') +
368 | '\n\n' +
369 | `${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
370 | `${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
371 | {
372 | padding: 1,
373 | borderColor: 'cyan',
374 | borderStyle: 'round',
375 | margin: { top: 1 }
376 | }
377 | )
378 | );
379 |
380 | if (aiServiceResponse?.telemetryData) {
381 | displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
382 | }
383 | }
384 |
```
--------------------------------------------------------------------------------
/tests/unit/mcp/tools/initialize-project.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Tests for the initialize-project MCP tool
3 | *
4 | * Note: This test does NOT test the actual implementation. It tests that:
5 | * 1. The tool is registered correctly with the correct parameters
6 | * 2. Command construction works correctly with various arguments
7 | * 3. Error handling works as expected
8 | * 4. Response formatting is correct
9 | *
10 | * We do NOT import the real implementation - everything is mocked
11 | */
12 |
13 | import { jest } from '@jest/globals';
14 |
15 | // Mock child_process.execSync
16 | const mockExecSync = jest.fn();
17 | jest.mock('child_process', () => ({
18 | execSync: mockExecSync
19 | }));
20 |
21 | // Mock the utility functions
22 | const mockCreateContentResponse = jest.fn((content) => ({
23 | content
24 | }));
25 |
26 | const mockCreateErrorResponse = jest.fn((message, details) => ({
27 | error: { message, details }
28 | }));
29 |
30 | jest.mock('../../../../mcp-server/src/tools/utils.js', () => ({
31 | createContentResponse: mockCreateContentResponse,
32 | createErrorResponse: mockCreateErrorResponse
33 | }));
34 |
35 | // Mock the z object from zod
36 | const mockZod = {
37 | object: jest.fn(() => mockZod),
38 | string: jest.fn(() => mockZod),
39 | boolean: jest.fn(() => mockZod),
40 | optional: jest.fn(() => mockZod),
41 | default: jest.fn(() => mockZod),
42 | describe: jest.fn(() => mockZod),
43 | _def: {
44 | shape: () => ({
45 | projectName: {},
46 | projectDescription: {},
47 | projectVersion: {},
48 | authorName: {},
49 | skipInstall: {},
50 | addAliases: {},
51 | yes: {}
52 | })
53 | }
54 | };
55 |
56 | jest.mock('zod', () => ({
57 | z: mockZod
58 | }));
59 |
60 | // Create our own simplified version of the registerInitializeProjectTool function
61 | const registerInitializeProjectTool = (server) => {
62 | server.addTool({
63 | name: 'initialize_project',
64 | description:
65 | "Initializes a new Task Master project structure in the current working directory by running 'task-master init'.",
66 | parameters: mockZod,
67 | execute: async (args, { log }) => {
68 | try {
69 | log.info(
70 | `Executing initialize_project with args: ${JSON.stringify(args)}`
71 | );
72 |
73 | // Construct the command arguments
74 | let command = 'npx task-master init';
75 | const cliArgs = [];
76 | if (args.projectName) {
77 | cliArgs.push(`--name "${args.projectName.replace(/"/g, '\\"')}"`);
78 | }
79 | if (args.projectDescription) {
80 | cliArgs.push(
81 | `--description "${args.projectDescription.replace(/"/g, '\\"')}"`
82 | );
83 | }
84 | if (args.projectVersion) {
85 | cliArgs.push(
86 | `--version "${args.projectVersion.replace(/"/g, '\\"')}"`
87 | );
88 | }
89 | if (args.authorName) {
90 | cliArgs.push(`--author "${args.authorName.replace(/"/g, '\\"')}"`);
91 | }
92 | if (args.skipInstall) cliArgs.push('--skip-install');
93 | if (args.addAliases) cliArgs.push('--aliases');
94 | if (args.yes) cliArgs.push('--yes');
95 |
96 | command += ' ' + cliArgs.join(' ');
97 |
98 | log.info(`Constructed command: ${command}`);
99 |
100 | // Execute the command
101 | const output = mockExecSync(command, {
102 | encoding: 'utf8',
103 | stdio: 'pipe',
104 | timeout: 300000
105 | });
106 |
107 | log.info(`Initialization output:\n${output}`);
108 |
109 | // Return success response
110 | return mockCreateContentResponse({
111 | message: 'Project initialized successfully.',
112 | next_step:
113 | 'Now that the project is initialized, the next step is to create the tasks by parsing a PRD. This will create the tasks folder and the initial task files. The parse-prd tool will required a PRD file',
114 | output: output
115 | });
116 | } catch (error) {
117 | // Catch errors
118 | const errorMessage = `Project initialization failed: ${error.message}`;
119 | const errorDetails =
120 | error.stderr?.toString() || error.stdout?.toString() || error.message;
121 | log.error(`${errorMessage}\nDetails: ${errorDetails}`);
122 |
123 | // Return error response
124 | return mockCreateErrorResponse(errorMessage, { details: errorDetails });
125 | }
126 | }
127 | });
128 | };
129 |
130 | describe('Initialize Project MCP Tool', () => {
131 | // Mock server and logger
132 | let mockServer;
133 | let executeFunction;
134 |
135 | const mockLogger = {
136 | debug: jest.fn(),
137 | info: jest.fn(),
138 | warn: jest.fn(),
139 | error: jest.fn()
140 | };
141 |
142 | beforeEach(() => {
143 | // Clear all mocks before each test
144 | jest.clearAllMocks();
145 |
146 | // Create mock server
147 | mockServer = {
148 | addTool: jest.fn((config) => {
149 | executeFunction = config.execute;
150 | })
151 | };
152 |
153 | // Default mock behavior
154 | mockExecSync.mockReturnValue('Project initialized successfully.');
155 |
156 | // Register the tool to capture the tool definition
157 | registerInitializeProjectTool(mockServer);
158 | });
159 |
160 | test('registers the tool with correct name and parameters', () => {
161 | // Check that addTool was called
162 | expect(mockServer.addTool).toHaveBeenCalledTimes(1);
163 |
164 | // Extract the tool definition from the mock call
165 | const toolDefinition = mockServer.addTool.mock.calls[0][0];
166 |
167 | // Verify tool properties
168 | expect(toolDefinition.name).toBe('initialize_project');
169 | expect(toolDefinition.description).toContain(
170 | 'Initializes a new Task Master project'
171 | );
172 | expect(toolDefinition).toHaveProperty('parameters');
173 | expect(toolDefinition).toHaveProperty('execute');
174 | });
175 |
176 | test('constructs command with proper arguments', async () => {
177 | // Create arguments with all parameters
178 | const args = {
179 | projectName: 'Test Project',
180 | projectDescription: 'A project for testing',
181 | projectVersion: '1.0.0',
182 | authorName: 'Test Author',
183 | skipInstall: true,
184 | addAliases: true,
185 | yes: true
186 | };
187 |
188 | // Execute the tool
189 | await executeFunction(args, { log: mockLogger });
190 |
191 | // Verify execSync was called with the expected command
192 | expect(mockExecSync).toHaveBeenCalledTimes(1);
193 |
194 | const command = mockExecSync.mock.calls[0][0];
195 |
196 | // Check that the command includes npx task-master init
197 | expect(command).toContain('npx task-master init');
198 |
199 | // Verify each argument is correctly formatted in the command
200 | expect(command).toContain('--name "Test Project"');
201 | expect(command).toContain('--description "A project for testing"');
202 | expect(command).toContain('--version "1.0.0"');
203 | expect(command).toContain('--author "Test Author"');
204 | expect(command).toContain('--skip-install');
205 | expect(command).toContain('--aliases');
206 | expect(command).toContain('--yes');
207 | });
208 |
209 | test('properly escapes special characters in arguments', async () => {
210 | // Create arguments with special characters
211 | const args = {
212 | projectName: 'Test "Quoted" Project',
213 | projectDescription: 'A "special" project for testing'
214 | };
215 |
216 | // Execute the tool
217 | await executeFunction(args, { log: mockLogger });
218 |
219 | // Get the command that was executed
220 | const command = mockExecSync.mock.calls[0][0];
221 |
222 | // Verify quotes were properly escaped
223 | expect(command).toContain('--name "Test \\"Quoted\\" Project"');
224 | expect(command).toContain(
225 | '--description "A \\"special\\" project for testing"'
226 | );
227 | });
228 |
229 | test('returns success response when command succeeds', async () => {
230 | // Set up the mock to return specific output
231 | const outputMessage = 'Project initialized successfully.';
232 | mockExecSync.mockReturnValueOnce(outputMessage);
233 |
234 | // Execute the tool
235 | const result = await executeFunction({}, { log: mockLogger });
236 |
237 | // Verify createContentResponse was called with the right arguments
238 | expect(mockCreateContentResponse).toHaveBeenCalledWith(
239 | expect.objectContaining({
240 | message: 'Project initialized successfully.',
241 | next_step: expect.any(String),
242 | output: outputMessage
243 | })
244 | );
245 |
246 | // Verify the returned result has the expected structure
247 | expect(result).toHaveProperty('content');
248 | expect(result.content).toHaveProperty('message');
249 | expect(result.content).toHaveProperty('next_step');
250 | expect(result.content).toHaveProperty('output');
251 | expect(result.content.output).toBe(outputMessage);
252 | });
253 |
254 | test('returns error response when command fails', async () => {
255 | // Create an error to be thrown
256 | const error = new Error('Command failed');
257 | error.stdout = 'Some standard output';
258 | error.stderr = 'Some error output';
259 |
260 | // Make the mock throw the error
261 | mockExecSync.mockImplementationOnce(() => {
262 | throw error;
263 | });
264 |
265 | // Execute the tool
266 | const result = await executeFunction({}, { log: mockLogger });
267 |
268 | // Verify createErrorResponse was called with the right arguments
269 | expect(mockCreateErrorResponse).toHaveBeenCalledWith(
270 | 'Project initialization failed: Command failed',
271 | expect.objectContaining({
272 | details: 'Some error output'
273 | })
274 | );
275 |
276 | // Verify the returned result has the expected structure
277 | expect(result).toHaveProperty('error');
278 | expect(result.error).toHaveProperty('message');
279 | expect(result.error.message).toContain('Project initialization failed');
280 | });
281 |
282 | test('logs information about the execution', async () => {
283 | // Execute the tool
284 | await executeFunction({}, { log: mockLogger });
285 |
286 | // Verify that logging occurred
287 | expect(mockLogger.info).toHaveBeenCalledWith(
288 | expect.stringContaining('Executing initialize_project')
289 | );
290 | expect(mockLogger.info).toHaveBeenCalledWith(
291 | expect.stringContaining('Constructed command')
292 | );
293 | expect(mockLogger.info).toHaveBeenCalledWith(
294 | expect.stringContaining('Initialization output')
295 | );
296 | });
297 |
298 | test('uses fallback to stdout if stderr is not available in error', async () => {
299 | // Create an error with only stdout
300 | const error = new Error('Command failed');
301 | error.stdout = 'Some standard output with error details';
302 | // No stderr property
303 |
304 | // Make the mock throw the error
305 | mockExecSync.mockImplementationOnce(() => {
306 | throw error;
307 | });
308 |
309 | // Execute the tool
310 | await executeFunction({}, { log: mockLogger });
311 |
312 | // Verify createErrorResponse was called with stdout as details
313 | expect(mockCreateErrorResponse).toHaveBeenCalledWith(
314 | expect.any(String),
315 | expect.objectContaining({
316 | details: 'Some standard output with error details'
317 | })
318 | );
319 | });
320 |
321 | test('logs error details when command fails', async () => {
322 | // Create an error
323 | const error = new Error('Command failed');
324 | error.stderr = 'Some detailed error message';
325 |
326 | // Make the mock throw the error
327 | mockExecSync.mockImplementationOnce(() => {
328 | throw error;
329 | });
330 |
331 | // Execute the tool
332 | await executeFunction({}, { log: mockLogger });
333 |
334 | // Verify error logging
335 | expect(mockLogger.error).toHaveBeenCalledWith(
336 | expect.stringContaining('Project initialization failed')
337 | );
338 | expect(mockLogger.error).toHaveBeenCalledWith(
339 | expect.stringContaining('Some detailed error message')
340 | );
341 | });
342 | });
343 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer-kiro.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mock fs module before importing anything that uses it
4 | jest.mock('fs', () => ({
5 | readFileSync: jest.fn(),
6 | writeFileSync: jest.fn(),
7 | existsSync: jest.fn(),
8 | mkdirSync: jest.fn(),
9 | readdirSync: jest.fn(),
10 | copyFileSync: jest.fn()
11 | }));
12 |
13 | // Mock the log function
14 | jest.mock('../../../scripts/modules/utils.js', () => ({
15 | log: jest.fn(),
16 | isSilentMode: jest.fn().mockReturnValue(false)
17 | }));
18 |
19 | // Import modules after mocking
20 | import fs from 'fs';
21 | import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
22 | import { kiroProfile } from '../../../src/profiles/kiro.js';
23 |
24 | describe('Kiro Rule Transformer', () => {
25 | // Set up spies on the mocked modules
26 | const mockReadFileSync = jest.spyOn(fs, 'readFileSync');
27 | const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync');
28 | const mockExistsSync = jest.spyOn(fs, 'existsSync');
29 | const mockMkdirSync = jest.spyOn(fs, 'mkdirSync');
30 | const mockConsoleError = jest
31 | .spyOn(console, 'error')
32 | .mockImplementation(() => {});
33 | jest.spyOn(console, 'log').mockImplementation(() => {});
34 |
35 | beforeEach(() => {
36 | jest.clearAllMocks();
37 | // Setup default mocks
38 | mockReadFileSync.mockReturnValue('');
39 | mockWriteFileSync.mockImplementation(() => {});
40 | mockExistsSync.mockReturnValue(true);
41 | mockMkdirSync.mockImplementation(() => {});
42 | });
43 |
44 | afterAll(() => {
45 | jest.restoreAllMocks();
46 | });
47 |
48 | it('should correctly convert basic terms', () => {
49 | const testContent = `---
50 | description: Test Cursor rule for basic terms
51 | globs: **/*
52 | alwaysApply: true
53 | ---
54 |
55 | This is a Cursor rule that references cursor.so and uses the word Cursor multiple times.
56 | Also has references to .mdc files.`;
57 |
58 | // Mock file read to return our test content
59 | mockReadFileSync.mockReturnValue(testContent);
60 |
61 | // Mock file system operations
62 | mockExistsSync.mockReturnValue(true);
63 |
64 | // Call the function
65 | const result = convertRuleToProfileRule(
66 | 'test-source.mdc',
67 | 'test-target.md',
68 | kiroProfile
69 | );
70 |
71 | // Verify the result
72 | expect(result).toBe(true);
73 | expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
74 |
75 | // Get the transformed content
76 | const transformedContent = mockWriteFileSync.mock.calls[0][1];
77 |
78 | // Verify Cursor -> Kiro transformations
79 | expect(transformedContent).toContain('kiro.dev');
80 | expect(transformedContent).toContain('Kiro');
81 | expect(transformedContent).not.toContain('cursor.so');
82 | expect(transformedContent).not.toContain('Cursor');
83 | expect(transformedContent).toContain('.md');
84 | expect(transformedContent).not.toContain('.mdc');
85 | });
86 |
87 | it('should handle URL transformations', () => {
88 | const testContent = `Visit https://cursor.so/docs for more information.
89 | Also check out cursor.so and www.cursor.so for updates.`;
90 |
91 | mockReadFileSync.mockReturnValue(testContent);
92 | mockExistsSync.mockReturnValue(true);
93 |
94 | const result = convertRuleToProfileRule(
95 | 'test-source.mdc',
96 | 'test-target.md',
97 | kiroProfile
98 | );
99 |
100 | expect(result).toBe(true);
101 | const transformedContent = mockWriteFileSync.mock.calls[0][1];
102 |
103 | // Verify URL transformations
104 | expect(transformedContent).toContain('https://kiro.dev');
105 | expect(transformedContent).toContain('kiro.dev');
106 | expect(transformedContent).not.toContain('cursor.so');
107 | });
108 |
109 | it('should handle file extension transformations', () => {
110 | const testContent = `This rule references file.mdc and another.mdc file.
111 | Use the .mdc extension for all rule files.`;
112 |
113 | mockReadFileSync.mockReturnValue(testContent);
114 | mockExistsSync.mockReturnValue(true);
115 |
116 | const result = convertRuleToProfileRule(
117 | 'test-source.mdc',
118 | 'test-target.md',
119 | kiroProfile
120 | );
121 |
122 | expect(result).toBe(true);
123 | const transformedContent = mockWriteFileSync.mock.calls[0][1];
124 |
125 | // Verify file extension transformations
126 | expect(transformedContent).toContain('file.md');
127 | expect(transformedContent).toContain('another.md');
128 | expect(transformedContent).toContain('.md extension');
129 | expect(transformedContent).not.toContain('.mdc');
130 | });
131 |
132 | it('should handle case variations', () => {
133 | const testContent = `CURSOR, Cursor, cursor should all be transformed.`;
134 |
135 | mockReadFileSync.mockReturnValue(testContent);
136 | mockExistsSync.mockReturnValue(true);
137 |
138 | const result = convertRuleToProfileRule(
139 | 'test-source.mdc',
140 | 'test-target.md',
141 | kiroProfile
142 | );
143 |
144 | expect(result).toBe(true);
145 | const transformedContent = mockWriteFileSync.mock.calls[0][1];
146 |
147 | // Verify case transformations
148 | // Due to regex order, the case-insensitive rule runs first:
149 | // CURSOR -> Kiro (because it starts with 'C'), Cursor -> Kiro, cursor -> kiro
150 | expect(transformedContent).toContain('Kiro');
151 | expect(transformedContent).toContain('kiro');
152 | expect(transformedContent).not.toContain('CURSOR');
153 | expect(transformedContent).not.toContain('Cursor');
154 | expect(transformedContent).not.toContain('cursor');
155 | });
156 |
157 | it('should create target directory if it does not exist', () => {
158 | const testContent = 'Test content';
159 | mockReadFileSync.mockReturnValue(testContent);
160 | mockExistsSync.mockReturnValue(false);
161 |
162 | const result = convertRuleToProfileRule(
163 | 'test-source.mdc',
164 | 'nested/path/test-target.md',
165 | kiroProfile
166 | );
167 |
168 | expect(result).toBe(true);
169 | expect(mockMkdirSync).toHaveBeenCalledWith('nested/path', {
170 | recursive: true
171 | });
172 | });
173 |
174 | it('should handle file system errors gracefully', () => {
175 | mockReadFileSync.mockImplementation(() => {
176 | throw new Error('File not found');
177 | });
178 |
179 | const result = convertRuleToProfileRule(
180 | 'test-source.mdc',
181 | 'test-target.md',
182 | kiroProfile
183 | );
184 |
185 | expect(result).toBe(false);
186 | expect(mockConsoleError).toHaveBeenCalledWith(
187 | 'Error converting rule file: File not found'
188 | );
189 | });
190 |
191 | it('should handle write errors gracefully', () => {
192 | mockReadFileSync.mockReturnValue('Test content');
193 | mockWriteFileSync.mockImplementation(() => {
194 | throw new Error('Write permission denied');
195 | });
196 |
197 | const result = convertRuleToProfileRule(
198 | 'test-source.mdc',
199 | 'test-target.md',
200 | kiroProfile
201 | );
202 |
203 | expect(result).toBe(false);
204 | expect(mockConsoleError).toHaveBeenCalledWith(
205 | 'Error converting rule file: Write permission denied'
206 | );
207 | });
208 |
209 | it('should verify profile configuration', () => {
210 | expect(kiroProfile.profileName).toBe('kiro');
211 | expect(kiroProfile.displayName).toBe('Kiro');
212 | expect(kiroProfile.profileDir).toBe('.kiro');
213 | expect(kiroProfile.mcpConfig).toBe(true);
214 | expect(kiroProfile.mcpConfigName).toBe('settings/mcp.json');
215 | expect(kiroProfile.mcpConfigPath).toBe('.kiro/settings/mcp.json');
216 | expect(kiroProfile.includeDefaultRules).toBe(true);
217 | expect(kiroProfile.fileMap).toEqual({
218 | 'rules/cursor_rules.mdc': 'kiro_rules.md',
219 | 'rules/dev_workflow.mdc': 'dev_workflow.md',
220 | 'rules/self_improve.mdc': 'self_improve.md',
221 | 'rules/taskmaster.mdc': 'taskmaster.md',
222 | 'rules/taskmaster_hooks_workflow.mdc': 'taskmaster_hooks_workflow.md'
223 | });
224 | });
225 |
226 | describe('onPostConvert lifecycle hook', () => {
227 | const mockReaddirSync = jest.spyOn(fs, 'readdirSync');
228 | const mockCopyFileSync = jest.spyOn(fs, 'copyFileSync');
229 |
230 | beforeEach(() => {
231 | jest.clearAllMocks();
232 | // Setup default mock implementation that doesn't throw
233 | mockCopyFileSync.mockImplementation(() => {});
234 | });
235 |
236 | it('should copy hook files when kiro-hooks directory exists', () => {
237 | const projectRoot = '/test/project';
238 | const assetsDir = '/test/assets';
239 | const hookFiles = [
240 | 'tm-test-hook1.kiro.hook',
241 | 'tm-test-hook2.kiro.hook',
242 | 'not-a-hook.txt'
243 | ];
244 |
245 | // Mock directory existence
246 | mockExistsSync.mockImplementation((path) => {
247 | if (path === '/test/assets/kiro-hooks') return true;
248 | if (path === '/test/project/.kiro/hooks') return false;
249 | return true;
250 | });
251 |
252 | // Mock reading hook files
253 | mockReaddirSync.mockReturnValue(hookFiles);
254 |
255 | // Call the lifecycle hook
256 | kiroProfile.onPostConvertRulesProfile(projectRoot, assetsDir);
257 |
258 | // Verify hooks directory was created
259 | expect(mockMkdirSync).toHaveBeenCalledWith('/test/project/.kiro/hooks', {
260 | recursive: true
261 | });
262 |
263 | // Verify only .kiro.hook files were copied
264 | expect(mockCopyFileSync).toHaveBeenCalledTimes(2);
265 | expect(mockCopyFileSync).toHaveBeenCalledWith(
266 | '/test/assets/kiro-hooks/tm-test-hook1.kiro.hook',
267 | '/test/project/.kiro/hooks/tm-test-hook1.kiro.hook'
268 | );
269 | expect(mockCopyFileSync).toHaveBeenCalledWith(
270 | '/test/assets/kiro-hooks/tm-test-hook2.kiro.hook',
271 | '/test/project/.kiro/hooks/tm-test-hook2.kiro.hook'
272 | );
273 | });
274 |
275 | it('should handle case when hooks directory already exists', () => {
276 | const projectRoot = '/test/project';
277 | const assetsDir = '/test/assets';
278 | const hookFiles = ['tm-test-hook.kiro.hook'];
279 |
280 | // Mock all directories exist
281 | mockExistsSync.mockReturnValue(true);
282 | mockReaddirSync.mockReturnValue(hookFiles);
283 |
284 | // Call the lifecycle hook
285 | kiroProfile.onPostConvertRulesProfile(projectRoot, assetsDir);
286 |
287 | // Verify hooks directory was NOT created (already exists)
288 | expect(mockMkdirSync).not.toHaveBeenCalled();
289 |
290 | // Verify hook was copied
291 | expect(mockCopyFileSync).toHaveBeenCalledWith(
292 | '/test/assets/kiro-hooks/tm-test-hook.kiro.hook',
293 | '/test/project/.kiro/hooks/tm-test-hook.kiro.hook'
294 | );
295 | });
296 |
297 | it('should handle case when kiro-hooks source directory does not exist', () => {
298 | const projectRoot = '/test/project';
299 | const assetsDir = '/test/assets';
300 |
301 | // Mock source directory doesn't exist
302 | mockExistsSync.mockImplementation((path) => {
303 | if (path === '/test/assets/kiro-hooks') return false;
304 | return true;
305 | });
306 |
307 | // Call the lifecycle hook
308 | kiroProfile.onPostConvertRulesProfile(projectRoot, assetsDir);
309 |
310 | // Verify no files were copied
311 | expect(mockReaddirSync).not.toHaveBeenCalled();
312 | expect(mockCopyFileSync).not.toHaveBeenCalled();
313 | });
314 |
315 | it('should handle case when no hook files exist in source directory', () => {
316 | const projectRoot = '/test/project';
317 | const assetsDir = '/test/assets';
318 |
319 | // Mock directory exists but has no hook files
320 | mockExistsSync.mockReturnValue(true);
321 | mockReaddirSync.mockReturnValue(['readme.txt', 'config.json']);
322 |
323 | // Call the lifecycle hook
324 | kiroProfile.onPostConvertRulesProfile(projectRoot, assetsDir);
325 |
326 | // Verify no files were copied
327 | expect(mockCopyFileSync).not.toHaveBeenCalled();
328 | });
329 | });
330 | });
331 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/rule-transformer.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import {
2 | isValidProfile,
3 | getRulesProfile
4 | } from '../../../src/utils/rule-transformer.js';
5 | import { RULE_PROFILES } from '../../../src/constants/profiles.js';
6 | import path from 'path';
7 |
8 | describe('Rule Transformer - General', () => {
9 | describe('Profile Configuration Validation', () => {
10 | it('should use RULE_PROFILES as the single source of truth', () => {
11 | // Ensure RULE_PROFILES is properly defined and contains expected profiles
12 | expect(Array.isArray(RULE_PROFILES)).toBe(true);
13 | expect(RULE_PROFILES.length).toBeGreaterThan(0);
14 |
15 | // Verify expected profiles are present
16 | const expectedProfiles = [
17 | 'claude',
18 | 'cline',
19 | 'codex',
20 | 'cursor',
21 | 'gemini',
22 | 'kiro',
23 | 'opencode',
24 | 'roo',
25 | 'trae',
26 | 'vscode',
27 | 'windsurf',
28 | 'zed'
29 | ];
30 | expectedProfiles.forEach((profile) => {
31 | expect(RULE_PROFILES).toContain(profile);
32 | });
33 | });
34 |
35 | it('should validate profiles correctly with isValidProfile', () => {
36 | // Test valid profiles
37 | RULE_PROFILES.forEach((profile) => {
38 | expect(isValidProfile(profile)).toBe(true);
39 | });
40 |
41 | // Test invalid profiles
42 | expect(isValidProfile('invalid')).toBe(false);
43 | expect(isValidProfile('')).toBe(false);
44 | expect(isValidProfile(null)).toBe(false);
45 | expect(isValidProfile(undefined)).toBe(false);
46 | });
47 |
48 | it('should return correct rule profile with getRulesProfile', () => {
49 | // Test valid profiles
50 | RULE_PROFILES.forEach((profile) => {
51 | const profileConfig = getRulesProfile(profile);
52 | expect(profileConfig).toBeDefined();
53 | expect(profileConfig.profileName.toLowerCase()).toBe(profile);
54 | });
55 |
56 | // Test invalid profile - should return null
57 | expect(getRulesProfile('invalid')).toBeNull();
58 | });
59 | });
60 |
61 | describe('Profile Structure', () => {
62 | it('should have all required properties for each profile', () => {
63 | RULE_PROFILES.forEach((profile) => {
64 | const profileConfig = getRulesProfile(profile);
65 |
66 | // Check required properties
67 | expect(profileConfig).toHaveProperty('profileName');
68 | expect(profileConfig).toHaveProperty('conversionConfig');
69 | expect(profileConfig).toHaveProperty('fileMap');
70 | expect(profileConfig).toHaveProperty('rulesDir');
71 | expect(profileConfig).toHaveProperty('profileDir');
72 |
73 | // All profiles should have conversionConfig and fileMap objects
74 | expect(typeof profileConfig.conversionConfig).toBe('object');
75 | expect(typeof profileConfig.fileMap).toBe('object');
76 |
77 | // Check that conversionConfig has required structure for profiles with rules
78 | const hasRules = Object.keys(profileConfig.fileMap).length > 0;
79 | if (hasRules) {
80 | expect(profileConfig.conversionConfig).toHaveProperty('profileTerms');
81 | expect(profileConfig.conversionConfig).toHaveProperty('toolNames');
82 | expect(profileConfig.conversionConfig).toHaveProperty('toolContexts');
83 | expect(profileConfig.conversionConfig).toHaveProperty('toolGroups');
84 | expect(profileConfig.conversionConfig).toHaveProperty('docUrls');
85 | expect(profileConfig.conversionConfig).toHaveProperty(
86 | 'fileReferences'
87 | );
88 |
89 | // Verify arrays are actually arrays
90 | expect(
91 | Array.isArray(profileConfig.conversionConfig.profileTerms)
92 | ).toBe(true);
93 | expect(typeof profileConfig.conversionConfig.toolNames).toBe(
94 | 'object'
95 | );
96 | expect(
97 | Array.isArray(profileConfig.conversionConfig.toolContexts)
98 | ).toBe(true);
99 | expect(Array.isArray(profileConfig.conversionConfig.toolGroups)).toBe(
100 | true
101 | );
102 | expect(Array.isArray(profileConfig.conversionConfig.docUrls)).toBe(
103 | true
104 | );
105 | }
106 | });
107 | });
108 |
109 | it('should have valid fileMap with required files for each profile', () => {
110 | const expectedRuleFiles = [
111 | 'cursor_rules.mdc',
112 | 'dev_workflow.mdc',
113 | 'self_improve.mdc',
114 | 'taskmaster.mdc'
115 | ];
116 |
117 | RULE_PROFILES.forEach((profile) => {
118 | const profileConfig = getRulesProfile(profile);
119 |
120 | // Check that fileMap exists and is an object
121 | expect(profileConfig.fileMap).toBeDefined();
122 | expect(typeof profileConfig.fileMap).toBe('object');
123 | expect(profileConfig.fileMap).not.toBeNull();
124 |
125 | const fileMapKeys = Object.keys(profileConfig.fileMap);
126 |
127 | // All profiles should have some fileMap entries now
128 | expect(fileMapKeys.length).toBeGreaterThan(0);
129 |
130 | // Check if this profile has rule files or asset files
131 | const hasRuleFiles = expectedRuleFiles.some((file) =>
132 | fileMapKeys.includes(file)
133 | );
134 | const hasAssetFiles = fileMapKeys.some(
135 | (file) => !expectedRuleFiles.includes(file)
136 | );
137 |
138 | if (hasRuleFiles) {
139 | // Profiles with rule files should have all expected rule files
140 | expectedRuleFiles.forEach((expectedFile) => {
141 | expect(fileMapKeys).toContain(expectedFile);
142 | expect(typeof profileConfig.fileMap[expectedFile]).toBe('string');
143 | expect(profileConfig.fileMap[expectedFile].length).toBeGreaterThan(
144 | 0
145 | );
146 | });
147 | }
148 |
149 | if (hasAssetFiles) {
150 | // Profiles with asset files (like Claude/Codex) should have valid asset mappings
151 | fileMapKeys.forEach((key) => {
152 | expect(typeof profileConfig.fileMap[key]).toBe('string');
153 | expect(profileConfig.fileMap[key].length).toBeGreaterThan(0);
154 | });
155 | }
156 | });
157 | });
158 | });
159 |
160 | describe('MCP Configuration Properties', () => {
161 | it('should have all required MCP properties for each profile', () => {
162 | RULE_PROFILES.forEach((profile) => {
163 | const profileConfig = getRulesProfile(profile);
164 |
165 | // Check MCP-related properties exist
166 | expect(profileConfig).toHaveProperty('mcpConfig');
167 | expect(profileConfig).toHaveProperty('mcpConfigName');
168 | expect(profileConfig).toHaveProperty('mcpConfigPath');
169 |
170 | // Check types based on MCP configuration
171 | expect(typeof profileConfig.mcpConfig).toBe('boolean');
172 |
173 | if (profileConfig.mcpConfig !== false) {
174 | // Check that mcpConfigPath is properly constructed
175 | const expectedPath = path.join(
176 | profileConfig.profileDir,
177 | profileConfig.mcpConfigName
178 | );
179 | expect(profileConfig.mcpConfigPath).toBe(expectedPath);
180 | }
181 | });
182 | });
183 |
184 | it('should have correct MCP configuration for each profile', () => {
185 | const expectedConfigs = {
186 | amp: {
187 | mcpConfig: true,
188 | mcpConfigName: 'settings.json',
189 | expectedPath: '.vscode/settings.json'
190 | },
191 | claude: {
192 | mcpConfig: true,
193 | mcpConfigName: '.mcp.json',
194 | expectedPath: '.mcp.json'
195 | },
196 | cline: {
197 | mcpConfig: false,
198 | mcpConfigName: null,
199 | expectedPath: null
200 | },
201 | codex: {
202 | mcpConfig: false,
203 | mcpConfigName: null,
204 | expectedPath: null
205 | },
206 | cursor: {
207 | mcpConfig: true,
208 | mcpConfigName: 'mcp.json',
209 | expectedPath: '.cursor/mcp.json'
210 | },
211 | gemini: {
212 | mcpConfig: true,
213 | mcpConfigName: 'settings.json',
214 | expectedPath: '.gemini/settings.json'
215 | },
216 | kiro: {
217 | mcpConfig: true,
218 | mcpConfigName: 'settings/mcp.json',
219 | expectedPath: '.kiro/settings/mcp.json'
220 | },
221 | opencode: {
222 | mcpConfig: true,
223 | mcpConfigName: 'opencode.json',
224 | expectedPath: 'opencode.json'
225 | },
226 | roo: {
227 | mcpConfig: true,
228 | mcpConfigName: 'mcp.json',
229 | expectedPath: '.roo/mcp.json'
230 | },
231 | kilo: {
232 | mcpConfig: true,
233 | mcpConfigName: 'mcp.json',
234 | expectedPath: '.kilo/mcp.json'
235 | },
236 | trae: {
237 | mcpConfig: false,
238 | mcpConfigName: null,
239 | expectedPath: null
240 | },
241 | vscode: {
242 | mcpConfig: true,
243 | mcpConfigName: 'mcp.json',
244 | expectedPath: '.vscode/mcp.json'
245 | },
246 | windsurf: {
247 | mcpConfig: true,
248 | mcpConfigName: 'mcp.json',
249 | expectedPath: '.windsurf/mcp.json'
250 | },
251 | zed: {
252 | mcpConfig: true,
253 | mcpConfigName: 'settings.json',
254 | expectedPath: '.zed/settings.json'
255 | }
256 | };
257 |
258 | RULE_PROFILES.forEach((profile) => {
259 | const profileConfig = getRulesProfile(profile);
260 | const expected = expectedConfigs[profile];
261 |
262 | expect(profileConfig.mcpConfig).toBe(expected.mcpConfig);
263 | expect(profileConfig.mcpConfigName).toBe(expected.mcpConfigName);
264 | expect(profileConfig.mcpConfigPath).toBe(expected.expectedPath);
265 | });
266 | });
267 |
268 | it('should have consistent profileDir and mcpConfigPath relationship', () => {
269 | RULE_PROFILES.forEach((profile) => {
270 | const profileConfig = getRulesProfile(profile);
271 | if (profileConfig.mcpConfig !== false) {
272 | // Profiles with MCP configuration should have valid paths
273 | // Handle root directory profiles differently
274 | if (profileConfig.profileDir === '.') {
275 | if (profile === 'claude') {
276 | // Claude explicitly uses '.mcp.json'
277 | expect(profileConfig.mcpConfigPath).toBe('.mcp.json');
278 | } else {
279 | // Other root profiles normalize to just the filename
280 | expect(profileConfig.mcpConfigPath).toBe(
281 | profileConfig.mcpConfigName
282 | );
283 | }
284 | } else {
285 | // Non-root profiles should have profileDir/configName pattern
286 | expect(profileConfig.mcpConfigPath).toMatch(
287 | new RegExp(
288 | `^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`
289 | )
290 | );
291 | }
292 | }
293 | });
294 | });
295 |
296 | it('should have unique profile directories', () => {
297 | const profileDirs = RULE_PROFILES.map((profile) => {
298 | const profileConfig = getRulesProfile(profile);
299 | return profileConfig.profileDir;
300 | });
301 |
302 | // Note: Claude and Codex both use "." (root directory) so we expect some duplication
303 | const uniqueProfileDirs = [...new Set(profileDirs)];
304 | // We should have fewer unique directories than total profiles due to simple profiles using root
305 | expect(uniqueProfileDirs.length).toBeLessThanOrEqual(profileDirs.length);
306 | expect(uniqueProfileDirs.length).toBeGreaterThan(0);
307 | });
308 |
309 | it('should have unique MCP config paths', () => {
310 | const mcpConfigPaths = RULE_PROFILES.map((profile) => {
311 | const profileConfig = getRulesProfile(profile);
312 | return profileConfig.mcpConfigPath;
313 | });
314 |
315 | // Note: Claude and Codex both have null mcpConfigPath so we expect some duplication
316 | const uniqueMcpConfigPaths = [...new Set(mcpConfigPaths)];
317 | // We should have fewer unique paths than total profiles due to simple profiles having null
318 | expect(uniqueMcpConfigPaths.length).toBeLessThanOrEqual(
319 | mcpConfigPaths.length
320 | );
321 | expect(uniqueMcpConfigPaths.length).toBeGreaterThan(0);
322 | });
323 | });
324 | });
325 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/ai/providers/base-provider.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Abstract base provider with Template Method pattern for AI providers
3 | * Provides common functionality, error handling, and retry logic
4 | */
5 |
6 | import {
7 | ERROR_CODES,
8 | TaskMasterError
9 | } from '../../../common/errors/task-master-error.js';
10 | import type {
11 | AIModel,
12 | AIOptions,
13 | AIResponse,
14 | IAIProvider,
15 | ProviderInfo,
16 | ProviderUsageStats
17 | } from '../interfaces/ai-provider.interface.js';
18 |
19 | // Constants for retry logic
20 | const DEFAULT_MAX_RETRIES = 3;
21 | const BASE_RETRY_DELAY_MS = 1000;
22 | const MAX_RETRY_DELAY_MS = 32000;
23 | const BACKOFF_MULTIPLIER = 2;
24 | const JITTER_FACTOR = 0.1;
25 |
26 | // Constants for validation
27 | const MIN_PROMPT_LENGTH = 1;
28 | const MAX_PROMPT_LENGTH = 100000;
29 | const MIN_TEMPERATURE = 0;
30 | const MAX_TEMPERATURE = 2;
31 | const MIN_MAX_TOKENS = 1;
32 | const MAX_MAX_TOKENS = 131072;
33 |
34 | /**
35 | * Configuration for BaseProvider
36 | */
37 | export interface BaseProviderConfig {
38 | apiKey: string;
39 | model?: string;
40 | }
41 |
42 | /**
43 | * Internal completion result structure
44 | */
45 | export interface CompletionResult {
46 | content: string;
47 | inputTokens?: number;
48 | outputTokens?: number;
49 | finishReason?: string;
50 | model?: string;
51 | }
52 |
53 | /**
54 | * Validation result for input validation
55 | */
56 | interface ValidationResult {
57 | valid: boolean;
58 | error?: string;
59 | }
60 |
61 | /**
62 | * Prepared request after preprocessing
63 | */
64 | interface PreparedRequest {
65 | prompt: string;
66 | options: AIOptions;
67 | metadata: Record<string, any>;
68 | }
69 |
70 | /**
71 | * Abstract base provider implementing Template Method pattern
72 | * Provides common error handling, retry logic, and validation
73 | */
74 | export abstract class BaseProvider implements IAIProvider {
75 | protected readonly apiKey: string;
76 | protected model: string;
77 |
78 | constructor(config: BaseProviderConfig) {
79 | if (!config.apiKey) {
80 | throw new TaskMasterError(
81 | 'API key is required',
82 | ERROR_CODES.AUTHENTICATION_ERROR
83 | );
84 | }
85 | this.apiKey = config.apiKey;
86 | this.model = config.model || this.getDefaultModel();
87 | }
88 |
89 | /**
90 | * Template method for generating completions
91 | * Handles validation, retries, and error handling
92 | */
93 | async generateCompletion(
94 | prompt: string,
95 | options?: AIOptions
96 | ): Promise<AIResponse> {
97 | // Validate input
98 | const validation = this.validateInput(prompt, options);
99 | if (!validation.valid) {
100 | throw new TaskMasterError(
101 | validation.error || 'Invalid input',
102 | ERROR_CODES.VALIDATION_ERROR
103 | );
104 | }
105 |
106 | // Prepare request
107 | const prepared = this.prepareRequest(prompt, options);
108 |
109 | // Execute with retry logic
110 | let lastError: Error | undefined;
111 | const maxRetries = this.getMaxRetries();
112 |
113 | for (let attempt = 1; attempt <= maxRetries; attempt++) {
114 | try {
115 | const startTime = Date.now();
116 | const result = await this.generateCompletionInternal(
117 | prepared.prompt,
118 | prepared.options
119 | );
120 |
121 | const duration = Date.now() - startTime;
122 | return this.handleResponse(result, duration, prepared);
123 | } catch (error) {
124 | lastError = error as Error;
125 |
126 | if (!this.shouldRetry(error, attempt)) {
127 | break;
128 | }
129 |
130 | const delay = this.calculateBackoffDelay(attempt);
131 | await this.sleep(delay);
132 | }
133 | }
134 |
135 | // All retries failed
136 | this.handleError(lastError || new Error('Unknown error'));
137 | }
138 |
139 | /**
140 | * Validate input prompt and options
141 | */
142 | protected validateInput(
143 | prompt: string,
144 | options?: AIOptions
145 | ): ValidationResult {
146 | // Validate prompt
147 | if (!prompt || typeof prompt !== 'string') {
148 | return { valid: false, error: 'Prompt must be a non-empty string' };
149 | }
150 |
151 | const trimmedPrompt = prompt.trim();
152 | if (trimmedPrompt.length < MIN_PROMPT_LENGTH) {
153 | return { valid: false, error: 'Prompt cannot be empty' };
154 | }
155 |
156 | if (trimmedPrompt.length > MAX_PROMPT_LENGTH) {
157 | return {
158 | valid: false,
159 | error: `Prompt exceeds maximum length of ${MAX_PROMPT_LENGTH} characters`
160 | };
161 | }
162 |
163 | // Validate options if provided
164 | if (options) {
165 | const optionValidation = this.validateOptions(options);
166 | if (!optionValidation.valid) {
167 | return optionValidation;
168 | }
169 | }
170 |
171 | return { valid: true };
172 | }
173 |
174 | /**
175 | * Validate completion options
176 | */
177 | protected validateOptions(options: AIOptions): ValidationResult {
178 | if (options.temperature !== undefined) {
179 | if (
180 | options.temperature < MIN_TEMPERATURE ||
181 | options.temperature > MAX_TEMPERATURE
182 | ) {
183 | return {
184 | valid: false,
185 | error: `Temperature must be between ${MIN_TEMPERATURE} and ${MAX_TEMPERATURE}`
186 | };
187 | }
188 | }
189 |
190 | if (options.maxTokens !== undefined) {
191 | if (
192 | options.maxTokens < MIN_MAX_TOKENS ||
193 | options.maxTokens > MAX_MAX_TOKENS
194 | ) {
195 | return {
196 | valid: false,
197 | error: `Max tokens must be between ${MIN_MAX_TOKENS} and ${MAX_MAX_TOKENS}`
198 | };
199 | }
200 | }
201 |
202 | if (options.topP !== undefined) {
203 | if (options.topP < 0 || options.topP > 1) {
204 | return { valid: false, error: 'Top-p must be between 0 and 1' };
205 | }
206 | }
207 |
208 | return { valid: true };
209 | }
210 |
211 | /**
212 | * Prepare request for processing
213 | */
214 | protected prepareRequest(
215 | prompt: string,
216 | options?: AIOptions
217 | ): PreparedRequest {
218 | const defaultOptions = this.getDefaultOptions();
219 | const mergedOptions = { ...defaultOptions, ...options };
220 |
221 | return {
222 | prompt: prompt.trim(),
223 | options: mergedOptions,
224 | metadata: {
225 | provider: this.getName(),
226 | model: this.model,
227 | timestamp: new Date().toISOString()
228 | }
229 | };
230 | }
231 |
232 | /**
233 | * Process and format the response
234 | */
235 | protected handleResponse(
236 | result: CompletionResult,
237 | duration: number,
238 | request: PreparedRequest
239 | ): AIResponse {
240 | const inputTokens =
241 | result.inputTokens || this.calculateTokens(request.prompt);
242 | const outputTokens =
243 | result.outputTokens || this.calculateTokens(result.content);
244 |
245 | return {
246 | content: result.content,
247 | inputTokens,
248 | outputTokens,
249 | totalTokens: inputTokens + outputTokens,
250 | model: result.model || this.model,
251 | provider: this.getName(),
252 | timestamp: request.metadata.timestamp,
253 | duration,
254 | finishReason: result.finishReason
255 | };
256 | }
257 |
258 | /**
259 | * Handle errors with proper wrapping
260 | */
261 | protected handleError(error: unknown): never {
262 | if (error instanceof TaskMasterError) {
263 | throw error;
264 | }
265 |
266 | const errorMessage = error instanceof Error ? error.message : String(error);
267 | const errorCode = this.getErrorCode(error);
268 |
269 | throw new TaskMasterError(
270 | `${this.getName()} provider error: ${errorMessage}`,
271 | errorCode,
272 | {
273 | operation: 'generateCompletion',
274 | resource: this.getName(),
275 | details:
276 | error instanceof Error
277 | ? {
278 | name: error.name,
279 | stack: error.stack,
280 | model: this.model
281 | }
282 | : { error: String(error), model: this.model }
283 | },
284 | error instanceof Error ? error : undefined
285 | );
286 | }
287 |
288 | /**
289 | * Determine if request should be retried
290 | */
291 | protected shouldRetry(error: unknown, attempt: number): boolean {
292 | if (attempt >= this.getMaxRetries()) {
293 | return false;
294 | }
295 |
296 | return this.isRetryableError(error);
297 | }
298 |
299 | /**
300 | * Check if error is retryable
301 | */
302 | protected isRetryableError(error: unknown): boolean {
303 | if (this.isRateLimitError(error)) return true;
304 | if (this.isTimeoutError(error)) return true;
305 | if (this.isNetworkError(error)) return true;
306 |
307 | return false;
308 | }
309 |
310 | /**
311 | * Check if error is a rate limit error
312 | */
313 | protected isRateLimitError(error: unknown): boolean {
314 | if (error instanceof Error) {
315 | const message = error.message.toLowerCase();
316 | return (
317 | message.includes('rate limit') ||
318 | message.includes('too many requests') ||
319 | message.includes('429')
320 | );
321 | }
322 | return false;
323 | }
324 |
325 | /**
326 | * Check if error is a timeout error
327 | */
328 | protected isTimeoutError(error: unknown): boolean {
329 | if (error instanceof Error) {
330 | const message = error.message.toLowerCase();
331 | return (
332 | message.includes('timeout') ||
333 | message.includes('timed out') ||
334 | message.includes('econnreset')
335 | );
336 | }
337 | return false;
338 | }
339 |
340 | /**
341 | * Check if error is a network error
342 | */
343 | protected isNetworkError(error: unknown): boolean {
344 | if (error instanceof Error) {
345 | const message = error.message.toLowerCase();
346 | return (
347 | message.includes('network') ||
348 | message.includes('enotfound') ||
349 | message.includes('econnrefused')
350 | );
351 | }
352 | return false;
353 | }
354 |
355 | /**
356 | * Calculate exponential backoff delay with jitter
357 | */
358 | protected calculateBackoffDelay(attempt: number): number {
359 | const exponentialDelay =
360 | BASE_RETRY_DELAY_MS * BACKOFF_MULTIPLIER ** (attempt - 1);
361 | const clampedDelay = Math.min(exponentialDelay, MAX_RETRY_DELAY_MS);
362 |
363 | // Add jitter to prevent thundering herd
364 | const jitter = clampedDelay * JITTER_FACTOR * (Math.random() - 0.5) * 2;
365 |
366 | return Math.round(clampedDelay + jitter);
367 | }
368 |
369 | /**
370 | * Get error code from error
371 | */
372 | protected getErrorCode(error: unknown): string {
373 | if (this.isRateLimitError(error)) return ERROR_CODES.API_ERROR;
374 | if (this.isTimeoutError(error)) return ERROR_CODES.NETWORK_ERROR;
375 | if (this.isNetworkError(error)) return ERROR_CODES.NETWORK_ERROR;
376 |
377 | if (error instanceof Error && error.message.includes('401')) {
378 | return ERROR_CODES.AUTHENTICATION_ERROR;
379 | }
380 |
381 | return ERROR_CODES.PROVIDER_ERROR;
382 | }
383 |
384 | /**
385 | * Sleep utility for delays
386 | */
387 | protected sleep(ms: number): Promise<void> {
388 | return new Promise((resolve) => setTimeout(resolve, ms));
389 | }
390 |
391 | /**
392 | * Get default options for completions
393 | */
394 | protected getDefaultOptions(): AIOptions {
395 | return {
396 | temperature: 0.7,
397 | maxTokens: 2000,
398 | topP: 1.0
399 | };
400 | }
401 |
402 | /**
403 | * Get maximum retry attempts
404 | */
405 | protected getMaxRetries(): number {
406 | return DEFAULT_MAX_RETRIES;
407 | }
408 |
409 | // Public interface methods
410 | getModel(): string {
411 | return this.model;
412 | }
413 |
414 | setModel(model: string): void {
415 | this.model = model;
416 | }
417 |
418 | // Abstract methods that must be implemented by concrete providers
419 | protected abstract generateCompletionInternal(
420 | prompt: string,
421 | options?: AIOptions
422 | ): Promise<CompletionResult>;
423 |
424 | abstract calculateTokens(text: string, model?: string): number;
425 | abstract getName(): string;
426 | abstract getDefaultModel(): string;
427 |
428 | // IAIProvider methods that must be implemented
429 | abstract generateStreamingCompletion(
430 | prompt: string,
431 | options?: AIOptions
432 | ): AsyncIterator<Partial<AIResponse>>;
433 | abstract isAvailable(): Promise<boolean>;
434 | abstract getProviderInfo(): ProviderInfo;
435 | abstract getAvailableModels(): AIModel[];
436 | abstract validateCredentials(): Promise<boolean>;
437 | abstract getUsageStats(): Promise<ProviderUsageStats | null>;
438 | abstract initialize(): Promise<void>;
439 | abstract close(): Promise<void>;
440 | }
441 |
```
--------------------------------------------------------------------------------
/scripts/modules/task-manager/update-tasks.js:
--------------------------------------------------------------------------------
```javascript
1 | import path from 'path';
2 | import chalk from 'chalk';
3 | import boxen from 'boxen';
4 | import Table from 'cli-table3';
5 |
6 | import {
7 | log as consoleLog,
8 | readJSON,
9 | writeJSON,
10 | truncate,
11 | isSilentMode
12 | } from '../utils.js';
13 |
14 | import {
15 | getStatusWithColor,
16 | startLoadingIndicator,
17 | stopLoadingIndicator,
18 | displayAiUsageSummary
19 | } from '../ui.js';
20 |
21 | import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js';
22 | import { getPromptManager } from '../prompt-manager.js';
23 | import generateTaskFiles from './generate-task-files.js';
24 | import { generateObjectService } from '../ai-services-unified.js';
25 | import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
26 | import { getModelConfiguration } from './models.js';
27 | import { ContextGatherer } from '../utils/contextGatherer.js';
28 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
29 | import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js';
30 |
31 | /**
32 | * Update tasks based on new context using the unified AI service.
33 | * @param {string} tasksPath - Path to the tasks.json file
34 | * @param {number} fromId - Task ID to start updating from
35 | * @param {string} prompt - Prompt with new context
36 | * @param {boolean} [useResearch=false] - Whether to use the research AI role.
37 | * @param {Object} context - Context object containing session and mcpLog.
38 | * @param {Object} [context.session] - Session object from MCP server.
39 | * @param {Object} [context.mcpLog] - MCP logger object.
40 | * @param {string} [context.tag] - Tag for the task
41 | * @param {string} [outputFormat='text'] - Output format ('text' or 'json').
42 | */
43 | async function updateTasks(
44 | tasksPath,
45 | fromId,
46 | prompt,
47 | useResearch = false,
48 | context = {},
49 | outputFormat = 'text' // Default to text for CLI
50 | ) {
51 | const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
52 | // Use mcpLog if available, otherwise use the imported consoleLog function
53 | const logFn = mcpLog || consoleLog;
54 | // Flag to easily check which logger type we have
55 | const isMCP = !!mcpLog;
56 |
57 | if (isMCP)
58 | logFn.info(`updateTasks called with context: session=${!!session}`);
59 | else logFn('info', `updateTasks called`); // CLI log
60 |
61 | try {
62 | if (isMCP) logFn.info(`Updating tasks from ID ${fromId}`);
63 | else
64 | logFn(
65 | 'info',
66 | `Updating tasks from ID ${fromId} with prompt: "${prompt}"`
67 | );
68 |
69 | // Determine project root
70 | const projectRoot = providedProjectRoot || findProjectRoot();
71 | if (!projectRoot) {
72 | throw new Error('Could not determine project root directory');
73 | }
74 |
75 | // --- Task Loading/Filtering (Updated to pass projectRoot and tag) ---
76 | const data = readJSON(tasksPath, projectRoot, tag);
77 | if (!data || !data.tasks)
78 | throw new Error(`No valid tasks found in ${tasksPath}`);
79 | const tasksToUpdate = data.tasks.filter(
80 | (task) => task.id >= fromId && task.status !== 'done'
81 | );
82 | if (tasksToUpdate.length === 0) {
83 | if (isMCP)
84 | logFn.info(`No tasks to update (ID >= ${fromId} and not 'done').`);
85 | else
86 | logFn('info', `No tasks to update (ID >= ${fromId} and not 'done').`);
87 | if (outputFormat === 'text') console.log(/* yellow message */);
88 | return; // Nothing to do
89 | }
90 | // --- End Task Loading/Filtering ---
91 |
92 | // --- Context Gathering ---
93 | let gatheredContext = '';
94 | try {
95 | const contextGatherer = new ContextGatherer(projectRoot, tag);
96 | const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
97 | const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update');
98 | const searchResults = fuzzySearch.findRelevantTasks(prompt, {
99 | maxResults: 5,
100 | includeSelf: true
101 | });
102 | const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
103 |
104 | const tasksToUpdateIds = tasksToUpdate.map((t) => t.id.toString());
105 | const finalTaskIds = [
106 | ...new Set([...tasksToUpdateIds, ...relevantTaskIds])
107 | ];
108 |
109 | if (finalTaskIds.length > 0) {
110 | const contextResult = await contextGatherer.gather({
111 | tasks: finalTaskIds,
112 | format: 'research'
113 | });
114 | gatheredContext = contextResult.context || '';
115 | }
116 | } catch (contextError) {
117 | logFn(
118 | 'warn',
119 | `Could not gather additional context: ${contextError.message}`
120 | );
121 | }
122 | // --- End Context Gathering ---
123 |
124 | // --- Display Tasks to Update (CLI Only - Unchanged) ---
125 | if (outputFormat === 'text') {
126 | // Show the tasks that will be updated
127 | const table = new Table({
128 | head: [
129 | chalk.cyan.bold('ID'),
130 | chalk.cyan.bold('Title'),
131 | chalk.cyan.bold('Status')
132 | ],
133 | colWidths: [5, 70, 20]
134 | });
135 |
136 | tasksToUpdate.forEach((task) => {
137 | table.push([
138 | task.id,
139 | truncate(task.title, 57),
140 | getStatusWithColor(task.status)
141 | ]);
142 | });
143 |
144 | console.log(
145 | boxen(chalk.white.bold(`Updating ${tasksToUpdate.length} tasks`), {
146 | padding: 1,
147 | borderColor: 'blue',
148 | borderStyle: 'round',
149 | margin: { top: 1, bottom: 0 }
150 | })
151 | );
152 |
153 | console.log(table.toString());
154 |
155 | // Display a message about how completed subtasks are handled
156 | console.log(
157 | boxen(
158 | chalk.cyan.bold('How Completed Subtasks Are Handled:') +
159 | '\n\n' +
160 | chalk.white(
161 | '• Subtasks marked as "done" or "completed" will be preserved\n'
162 | ) +
163 | chalk.white(
164 | '• New subtasks will build upon what has already been completed\n'
165 | ) +
166 | chalk.white(
167 | '• If completed work needs revision, a new subtask will be created instead of modifying done items\n'
168 | ) +
169 | chalk.white(
170 | '• This approach maintains a clear record of completed work and new requirements'
171 | ),
172 | {
173 | padding: 1,
174 | borderColor: 'blue',
175 | borderStyle: 'round',
176 | margin: { top: 1, bottom: 1 }
177 | }
178 | )
179 | );
180 | }
181 | // --- End Display Tasks ---
182 |
183 | // --- Build Prompts (Using PromptManager) ---
184 | // Load prompts using PromptManager
185 | const promptManager = getPromptManager();
186 | const { systemPrompt, userPrompt } = await promptManager.loadPrompt(
187 | 'update-tasks',
188 | {
189 | tasks: tasksToUpdate,
190 | updatePrompt: prompt,
191 | useResearch,
192 | projectContext: gatheredContext,
193 | hasCodebaseAnalysis: hasCodebaseAnalysis(
194 | useResearch,
195 | projectRoot,
196 | session
197 | ),
198 | projectRoot: projectRoot
199 | }
200 | );
201 | // --- End Build Prompts ---
202 |
203 | // --- AI Call ---
204 | let loadingIndicator = null;
205 | let aiServiceResponse = null;
206 |
207 | if (!isMCP && outputFormat === 'text') {
208 | loadingIndicator = startLoadingIndicator('Updating tasks with AI...\n');
209 | }
210 |
211 | try {
212 | // Determine role based on research flag
213 | const serviceRole = useResearch ? 'research' : 'main';
214 |
215 | // Call the unified AI service with generateObject
216 | aiServiceResponse = await generateObjectService({
217 | role: serviceRole,
218 | session: session,
219 | projectRoot: projectRoot,
220 | systemPrompt: systemPrompt,
221 | prompt: userPrompt,
222 | schema: COMMAND_SCHEMAS['update-tasks'],
223 | objectName: 'tasks',
224 | commandName: 'update-tasks',
225 | outputType: isMCP ? 'mcp' : 'cli'
226 | });
227 |
228 | if (loadingIndicator)
229 | stopLoadingIndicator(loadingIndicator, 'AI update complete.');
230 |
231 | // With generateObject, we get structured data directly
232 | const parsedUpdatedTasks = aiServiceResponse.mainResult.tasks;
233 |
234 | // --- Update Tasks Data (Updated writeJSON call) ---
235 | if (!Array.isArray(parsedUpdatedTasks)) {
236 | // Should be caught by parser, but extra check
237 | throw new Error(
238 | 'Parsed AI response for updated tasks was not an array.'
239 | );
240 | }
241 | if (isMCP)
242 | logFn.info(
243 | `Received ${parsedUpdatedTasks.length} updated tasks from AI.`
244 | );
245 | else
246 | logFn(
247 | 'info',
248 | `Received ${parsedUpdatedTasks.length} updated tasks from AI.`
249 | );
250 | // Create a map for efficient lookup
251 | const updatedTasksMap = new Map(
252 | parsedUpdatedTasks.map((task) => [task.id, task])
253 | );
254 |
255 | let actualUpdateCount = 0;
256 | data.tasks.forEach((task, index) => {
257 | if (updatedTasksMap.has(task.id)) {
258 | // Only update if the task was part of the set sent to AI
259 | const updatedTask = updatedTasksMap.get(task.id);
260 | // Merge the updated task with the existing one to preserve fields like subtasks
261 | data.tasks[index] = {
262 | ...task, // Keep all existing fields
263 | ...updatedTask, // Override with updated fields
264 | // Ensure subtasks field is preserved if not provided by AI
265 | subtasks:
266 | updatedTask.subtasks !== undefined
267 | ? updatedTask.subtasks
268 | : task.subtasks
269 | };
270 | actualUpdateCount++;
271 | }
272 | });
273 | if (isMCP)
274 | logFn.info(
275 | `Applied updates to ${actualUpdateCount} tasks in the dataset.`
276 | );
277 | else
278 | logFn(
279 | 'info',
280 | `Applied updates to ${actualUpdateCount} tasks in the dataset.`
281 | );
282 |
283 | // Fix: Pass projectRoot and currentTag to writeJSON
284 | writeJSON(tasksPath, data, projectRoot, tag);
285 | if (isMCP)
286 | logFn.info(
287 | `Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
288 | );
289 | else
290 | logFn(
291 | 'success',
292 | `Successfully updated ${actualUpdateCount} tasks in ${tasksPath}`
293 | );
294 | // await generateTaskFiles(tasksPath, path.dirname(tasksPath));
295 |
296 | if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
297 | displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
298 | }
299 |
300 | return {
301 | success: true,
302 | updatedTasks: parsedUpdatedTasks,
303 | telemetryData: aiServiceResponse.telemetryData,
304 | tagInfo: aiServiceResponse.tagInfo
305 | };
306 | } catch (error) {
307 | if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
308 | if (isMCP) logFn.error(`Error during AI service call: ${error.message}`);
309 | else logFn('error', `Error during AI service call: ${error.message}`);
310 | if (error.message.includes('API key')) {
311 | if (isMCP)
312 | logFn.error(
313 | 'Please ensure API keys are configured correctly in .env or mcp.json.'
314 | );
315 | else
316 | logFn(
317 | 'error',
318 | 'Please ensure API keys are configured correctly in .env or mcp.json.'
319 | );
320 | }
321 | throw error;
322 | } finally {
323 | if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
324 | }
325 | } catch (error) {
326 | // --- General Error Handling (Unchanged) ---
327 | if (isMCP) logFn.error(`Error updating tasks: ${error.message}`);
328 | else logFn('error', `Error updating tasks: ${error.message}`);
329 | if (outputFormat === 'text') {
330 | console.error(chalk.red(`Error: ${error.message}`));
331 | if (getDebugFlag(session)) {
332 | console.error(error);
333 | }
334 | process.exit(1);
335 | } else {
336 | throw error; // Re-throw for MCP/programmatic callers
337 | }
338 | // --- End General Error Handling ---
339 | }
340 | }
341 |
342 | export default updateTasks;
343 |
```