This is page 16 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/add-subtask.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Tests for the addSubtask function
3 | */
4 | import { jest } from '@jest/globals';
5 |
6 | // Mock dependencies before importing the module
7 | const mockUtils = {
8 | readJSON: jest.fn(),
9 | writeJSON: jest.fn(),
10 | log: jest.fn(),
11 | getCurrentTag: jest.fn()
12 | };
13 | const mockTaskManager = {
14 | isTaskDependentOn: jest.fn()
15 | };
16 | const mockGenerateTaskFiles = jest.fn();
17 |
18 | jest.unstable_mockModule(
19 | '../../../../../scripts/modules/utils.js',
20 | () => mockUtils
21 | );
22 | jest.unstable_mockModule(
23 | '../../../../../scripts/modules/task-manager.js',
24 | () => mockTaskManager
25 | );
26 | jest.unstable_mockModule(
27 | '../../../../../scripts/modules/task-manager/generate-task-files.js',
28 | () => ({
29 | default: mockGenerateTaskFiles
30 | })
31 | );
32 |
33 | const addSubtask = (
34 | await import('../../../../../scripts/modules/task-manager/add-subtask.js')
35 | ).default;
36 |
37 | describe('addSubtask function', () => {
38 | const multiTagData = {
39 | master: {
40 | tasks: [{ id: 1, title: 'Master Task', subtasks: [] }],
41 | metadata: { description: 'Master tasks' }
42 | },
43 | 'feature-branch': {
44 | tasks: [{ id: 1, title: 'Feature Task', subtasks: [] }],
45 | metadata: { description: 'Feature tasks' }
46 | }
47 | };
48 |
49 | beforeEach(() => {
50 | jest.clearAllMocks();
51 | mockTaskManager.isTaskDependentOn.mockReturnValue(false);
52 | });
53 |
54 | test('should add a new subtask and preserve other tags', async () => {
55 | const context = { projectRoot: '/fake/root', tag: 'feature-branch' };
56 | const newSubtaskData = { title: 'My New Subtask' };
57 | mockUtils.readJSON.mockReturnValueOnce({
58 | tasks: [{ id: 1, title: 'Feature Task', subtasks: [] }],
59 | metadata: { description: 'Feature tasks' }
60 | });
61 |
62 | await addSubtask('tasks.json', '1', null, newSubtaskData, true, context);
63 |
64 | expect(mockUtils.writeJSON).toHaveBeenCalledWith(
65 | 'tasks.json',
66 | expect.any(Object),
67 | '/fake/root',
68 | 'feature-branch'
69 | );
70 | const writtenData = mockUtils.writeJSON.mock.calls[0][1];
71 | const parentTask = writtenData.tasks.find((t) => t.id === 1);
72 | expect(parentTask.subtasks).toHaveLength(1);
73 | expect(parentTask.subtasks[0].title).toBe('My New Subtask');
74 | });
75 |
76 | test('should add a new subtask to a parent task', async () => {
77 | mockUtils.readJSON.mockReturnValueOnce({
78 | tasks: [{ id: 1, title: 'Parent Task', subtasks: [] }]
79 | });
80 | const context = {};
81 | const newSubtask = await addSubtask(
82 | 'tasks.json',
83 | '1',
84 | null,
85 | { title: 'New Subtask' },
86 | true,
87 | context
88 | );
89 | expect(newSubtask).toBeDefined();
90 | expect(newSubtask.id).toBe(1);
91 | expect(newSubtask.parentTaskId).toBe(1);
92 | expect(mockUtils.writeJSON).toHaveBeenCalled();
93 | const writeCallArgs = mockUtils.writeJSON.mock.calls[0][1]; // data is the second arg now
94 | const parentTask = writeCallArgs.tasks.find((t) => t.id === 1);
95 | expect(parentTask.subtasks).toHaveLength(1);
96 | expect(parentTask.subtasks[0].title).toBe('New Subtask');
97 | });
98 |
99 | test('should convert an existing task to a subtask', async () => {
100 | mockUtils.readJSON.mockReturnValueOnce({
101 | tasks: [
102 | { id: 1, title: 'Parent Task', subtasks: [] },
103 | { id: 2, title: 'Existing Task 2', subtasks: [] }
104 | ]
105 | });
106 | const context = {};
107 | const convertedSubtask = await addSubtask(
108 | 'tasks.json',
109 | '1',
110 | '2',
111 | null,
112 | true,
113 | context
114 | );
115 | expect(convertedSubtask.id).toBe(1);
116 | expect(convertedSubtask.parentTaskId).toBe(1);
117 | expect(convertedSubtask.title).toBe('Existing Task 2');
118 | expect(mockUtils.writeJSON).toHaveBeenCalled();
119 | const writeCallArgs = mockUtils.writeJSON.mock.calls[0][1];
120 | const parentTask = writeCallArgs.tasks.find((t) => t.id === 1);
121 | expect(parentTask.subtasks).toHaveLength(1);
122 | expect(parentTask.subtasks[0].title).toBe('Existing Task 2');
123 | });
124 |
125 | test('should throw an error if parent task does not exist', async () => {
126 | mockUtils.readJSON.mockReturnValueOnce({
127 | tasks: [{ id: 1, title: 'Task 1', subtasks: [] }]
128 | });
129 | const context = {};
130 | await expect(
131 | addSubtask(
132 | 'tasks.json',
133 | '99',
134 | null,
135 | { title: 'New Subtask' },
136 | true,
137 | context
138 | )
139 | ).rejects.toThrow('Parent task with ID 99 not found');
140 | });
141 |
142 | test('should throw an error if trying to convert a non-existent task', async () => {
143 | mockUtils.readJSON.mockReturnValueOnce({
144 | tasks: [{ id: 1, title: 'Parent Task', subtasks: [] }]
145 | });
146 | const context = {};
147 | await expect(
148 | addSubtask('tasks.json', '1', '99', null, true, context)
149 | ).rejects.toThrow('Task with ID 99 not found');
150 | });
151 |
152 | test('should throw an error for circular dependency', async () => {
153 | mockUtils.readJSON.mockReturnValueOnce({
154 | tasks: [
155 | { id: 1, title: 'Parent Task', subtasks: [] },
156 | { id: 2, title: 'Child Task', subtasks: [] }
157 | ]
158 | });
159 | mockTaskManager.isTaskDependentOn.mockImplementation(
160 | (tasks, parentTask, existingTaskIdNum) => {
161 | return parentTask.id === 1 && existingTaskIdNum === 2;
162 | }
163 | );
164 | const context = {};
165 | await expect(
166 | addSubtask('tasks.json', '1', '2', null, true, context)
167 | ).rejects.toThrow(
168 | 'Cannot create circular dependency: task 1 is already a subtask or dependent of task 2'
169 | );
170 | });
171 | });
172 |
```
--------------------------------------------------------------------------------
/apps/docs/archive/ai-development-workflow.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: "AI Development Workflow"
3 | description: "Learn how Task Master and Cursor AI work together to streamline your development workflow"
4 | ---
5 |
6 | <Tip>The Cursor agent is pre-configured (via the rules file) to follow this workflow</Tip>
7 |
8 | <AccordionGroup>
9 | <Accordion title="1. Task Discovery and Selection">
10 | Ask the agent to list available tasks:
11 |
12 | ```
13 | What tasks are available to work on next?
14 | ```
15 |
16 | The agent will:
17 |
18 | - Run `task-master list` to see all tasks
19 | - Run `task-master next` to determine the next task to work on
20 | - Analyze dependencies to determine which tasks are ready to be worked on
21 | - Prioritize tasks based on priority level and ID order
22 | - Suggest the next task(s) to implement
23 | </Accordion>
24 |
25 | <Accordion title="2. Task Implementation">
26 | When implementing a task, the agent will:
27 |
28 | - Reference the task's details section for implementation specifics
29 | - Consider dependencies on previous tasks
30 | - Follow the project's coding standards
31 | - Create appropriate tests based on the task's testStrategy
32 |
33 | You can ask:
34 |
35 | ```
36 | Let's implement task 3. What does it involve?
37 | ```
38 | </Accordion>
39 |
40 | <Accordion title="3. Task Verification">
41 | Before marking a task as complete, verify it according to:
42 |
43 | - The task's specified testStrategy
44 | - Any automated tests in the codebase
45 | - Manual verification if required
46 | </Accordion>
47 |
48 | <Accordion title="4. Task Completion">
49 | When a task is completed, tell the agent:
50 |
51 | ```
52 | Task 3 is now complete. Please update its status.
53 | ```
54 |
55 | The agent will execute:
56 |
57 | ```bash
58 | task-master set-status --id=3 --status=done
59 | ```
60 | </Accordion>
61 |
62 | <Accordion title="5. Handling Implementation Drift">
63 | If during implementation, you discover that:
64 |
65 | - The current approach differs significantly from what was planned
66 | - Future tasks need to be modified due to current implementation choices
67 | - New dependencies or requirements have emerged
68 |
69 | Tell the agent:
70 |
71 | ```
72 | We've changed our approach. We're now using Express instead of Fastify. Please update all future tasks to reflect this change.
73 | ```
74 |
75 | The agent will execute:
76 |
77 | ```bash
78 | task-master update --from=4 --prompt="Now we are using Express instead of Fastify."
79 | ```
80 |
81 | This will rewrite or re-scope subsequent tasks in tasks.json while preserving completed work.
82 | </Accordion>
83 |
84 | <Accordion title="6. Breaking Down Complex Tasks">
85 | For complex tasks that need more granularity:
86 |
87 | ```
88 | Task 5 seems complex. Can you break it down into subtasks?
89 | ```
90 |
91 | The agent will execute:
92 |
93 | ```bash
94 | task-master expand --id=5 --num=3
95 | ```
96 |
97 | You can provide additional context:
98 |
99 | ```
100 | Please break down task 5 with a focus on security considerations.
101 | ```
102 |
103 | The agent will execute:
104 |
105 | ```bash
106 | task-master expand --id=5 --prompt="Focus on security aspects"
107 | ```
108 |
109 | You can also expand all pending tasks:
110 |
111 | ```
112 | Please break down all pending tasks into subtasks.
113 | ```
114 |
115 | The agent will execute:
116 |
117 | ```bash
118 | task-master expand --all
119 | ```
120 |
121 | For research-backed subtask generation using Perplexity AI:
122 |
123 | ```
124 | Please break down task 5 using research-backed generation.
125 | ```
126 |
127 | The agent will execute:
128 |
129 | ```bash
130 | task-master expand --id=5 --research
131 | ```
132 | </Accordion>
133 | </AccordionGroup>
134 |
135 | ## Example Cursor AI Interactions
136 |
137 | <AccordionGroup>
138 | <Accordion title="Starting a new project">
139 | ```
140 | I've just initialized a new project with Claude Task Master. I have a PRD at scripts/prd.txt.
141 | Can you help me parse it and set up the initial tasks?
142 | ```
143 | </Accordion>
144 | <Accordion title="Working on tasks">
145 | ```
146 | What's the next task I should work on? Please consider dependencies and priorities.
147 | ```
148 | </Accordion>
149 | <Accordion title="Implementing a specific task">
150 | ```
151 | I'd like to implement task 4. Can you help me understand what needs to be done and how to approach it?
152 | ```
153 | </Accordion>
154 | <Accordion title="Managing subtasks">
155 | ```
156 | I need to regenerate the subtasks for task 3 with a different approach. Can you help me clear and regenerate them?
157 | ```
158 | </Accordion>
159 | <Accordion title="Handling changes">
160 | ```
161 | We've decided to use MongoDB instead of PostgreSQL. Can you update all future tasks to reflect this change?
162 | ```
163 | </Accordion>
164 | <Accordion title="Completing work">
165 | ```
166 | I've finished implementing the authentication system described in task 2. All tests are passing.
167 | Please mark it as complete and tell me what I should work on next.
168 | ```
169 | </Accordion>
170 | <Accordion title="Analyzing complexity">
171 | ```
172 | Can you analyze the complexity of our tasks to help me understand which ones need to be broken down further?
173 | ```
174 | </Accordion>
175 | <Accordion title="Viewing complexity report">
176 | ```
177 | Can you show me the complexity report in a more readable format?
178 | ```
179 | </Accordion>
180 | </AccordionGroup>
181 |
```
--------------------------------------------------------------------------------
/apps/extension/src/webview/reducers/appReducer.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Main application state reducer
3 | */
4 |
5 | import type { AppState, AppAction } from '../types';
6 | import { logger } from '../utils/logger';
7 |
8 | export const appReducer = (state: AppState, action: AppAction): AppState => {
9 | logger.debug(
10 | 'Reducer action:',
11 | action.type,
12 | 'payload' in action ? action.payload : 'no payload'
13 | );
14 | switch (action.type) {
15 | case 'SET_TASKS':
16 | const newTasks = Array.isArray(action.payload) ? action.payload : [];
17 | logger.debug('SET_TASKS reducer - updating tasks:', {
18 | oldCount: state.tasks.length,
19 | newCount: newTasks.length,
20 | newTasks
21 | });
22 | return {
23 | ...state,
24 | tasks: newTasks,
25 | loading: false,
26 | error: undefined
27 | };
28 | case 'SET_LOADING':
29 | return { ...state, loading: action.payload };
30 | case 'SET_ERROR':
31 | return { ...state, error: action.payload, loading: false };
32 | case 'CLEAR_ERROR':
33 | return { ...state, error: undefined };
34 | case 'INCREMENT_REQUEST_ID':
35 | return { ...state, requestId: state.requestId + 1 };
36 | case 'UPDATE_TASK_STATUS': {
37 | const { taskId, newStatus } = action.payload;
38 | return {
39 | ...state,
40 | tasks: state.tasks.map((task) =>
41 | task.id === taskId ? { ...task, status: newStatus } : task
42 | )
43 | };
44 | }
45 | case 'UPDATE_TASK_CONTENT': {
46 | const { taskId, updates } = action.payload;
47 | return {
48 | ...state,
49 | tasks: state.tasks.map((task) =>
50 | task.id === taskId ? { ...task, ...updates } : task
51 | )
52 | };
53 | }
54 | case 'SET_CONNECTION_STATUS':
55 | return {
56 | ...state,
57 | isConnected: action.payload.isConnected,
58 | connectionStatus: action.payload.status
59 | };
60 | case 'SET_EDITING_TASK':
61 | return {
62 | ...state,
63 | editingTask: action.payload
64 | };
65 | case 'SET_POLLING_STATUS':
66 | return {
67 | ...state,
68 | polling: {
69 | ...state.polling,
70 | isActive: action.payload.isActive,
71 | errorCount: action.payload.errorCount ?? state.polling.errorCount,
72 | lastUpdate: action.payload.isActive
73 | ? Date.now()
74 | : state.polling.lastUpdate
75 | }
76 | };
77 | case 'SET_USER_INTERACTING':
78 | return {
79 | ...state,
80 | polling: {
81 | ...state.polling,
82 | isUserInteracting: action.payload
83 | }
84 | };
85 | case 'TASKS_UPDATED_FROM_POLLING':
86 | return {
87 | ...state,
88 | tasks: Array.isArray(action.payload) ? action.payload : [],
89 | polling: {
90 | ...state.polling,
91 | lastUpdate: Date.now()
92 | }
93 | };
94 | case 'SET_NETWORK_STATUS':
95 | return {
96 | ...state,
97 | polling: {
98 | ...state.polling,
99 | isOfflineMode: action.payload.isOfflineMode,
100 | connectionStatus: action.payload.connectionStatus,
101 | reconnectAttempts:
102 | action.payload.reconnectAttempts !== undefined
103 | ? action.payload.reconnectAttempts
104 | : state.polling.reconnectAttempts,
105 | maxReconnectAttempts:
106 | action.payload.maxReconnectAttempts !== undefined
107 | ? action.payload.maxReconnectAttempts
108 | : state.polling.maxReconnectAttempts,
109 | lastSuccessfulConnection:
110 | action.payload.lastSuccessfulConnection !== undefined
111 | ? action.payload.lastSuccessfulConnection
112 | : state.polling.lastSuccessfulConnection
113 | }
114 | };
115 | case 'LOAD_CACHED_TASKS':
116 | return {
117 | ...state,
118 | tasks: Array.isArray(action.payload) ? action.payload : []
119 | };
120 | case 'ADD_TOAST':
121 | return {
122 | ...state,
123 | toastNotifications: [...state.toastNotifications, action.payload]
124 | };
125 | case 'REMOVE_TOAST':
126 | return {
127 | ...state,
128 | toastNotifications: state.toastNotifications.filter(
129 | (notification) => notification.id !== action.payload
130 | )
131 | };
132 | case 'CLEAR_ALL_TOASTS':
133 | return { ...state, toastNotifications: [] };
134 | case 'NAVIGATE_TO_TASK':
135 | logger.debug('📍 Reducer: Navigating to task:', action.payload);
136 | return {
137 | ...state,
138 | currentView: 'task-details',
139 | selectedTaskId: action.payload
140 | };
141 | case 'NAVIGATE_TO_KANBAN':
142 | logger.debug('📍 Reducer: Navigating to kanban');
143 | return { ...state, currentView: 'kanban', selectedTaskId: undefined };
144 | case 'NAVIGATE_TO_CONFIG':
145 | logger.debug('📍 Reducer: Navigating to config');
146 | return { ...state, currentView: 'config', selectedTaskId: undefined };
147 | case 'SET_CURRENT_TAG':
148 | return {
149 | ...state,
150 | currentTag: action.payload
151 | };
152 | case 'SET_AVAILABLE_TAGS':
153 | return {
154 | ...state,
155 | availableTags: action.payload
156 | };
157 | case 'SET_TAG_DATA':
158 | return {
159 | ...state,
160 | currentTag: action.payload.currentTag,
161 | availableTags: action.payload.availableTags
162 | };
163 | default:
164 | return state;
165 | }
166 | };
167 |
168 | export const initialState: AppState = {
169 | tasks: [],
170 | loading: true,
171 | requestId: 0,
172 | isConnected: false,
173 | connectionStatus: 'Connecting...',
174 | editingTask: { taskId: null },
175 | polling: {
176 | isActive: false,
177 | errorCount: 0,
178 | lastUpdate: undefined,
179 | isUserInteracting: false,
180 | isOfflineMode: false,
181 | reconnectAttempts: 0,
182 | maxReconnectAttempts: 0,
183 | lastSuccessfulConnection: undefined,
184 | connectionStatus: 'online'
185 | },
186 | toastNotifications: [],
187 | currentView: 'kanban',
188 | selectedTaskId: undefined,
189 | // Tag-related state
190 | currentTag: 'master',
191 | availableTags: ['master']
192 | };
193 |
```
--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/add-tag.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * add-tag.js
3 | * Direct function implementation for creating a new tag
4 | */
5 |
6 | import {
7 | createTag,
8 | createTagFromBranch
9 | } from '../../../../scripts/modules/task-manager/tag-management.js';
10 | import {
11 | enableSilentMode,
12 | disableSilentMode
13 | } from '../../../../scripts/modules/utils.js';
14 | import { createLogWrapper } from '../../tools/utils.js';
15 |
16 | /**
17 | * Direct function wrapper for creating a new tag with error handling.
18 | *
19 | * @param {Object} args - Command arguments
20 | * @param {string} args.name - Name of the new tag to create
21 | * @param {boolean} [args.copyFromCurrent=false] - Whether to copy tasks from current tag
22 | * @param {string} [args.copyFromTag] - Specific tag to copy tasks from
23 | * @param {boolean} [args.fromBranch=false] - Create tag name from current git branch
24 | * @param {string} [args.description] - Optional description for the tag
25 | * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool)
26 | * @param {string} [args.projectRoot] - Project root path
27 | * @param {Object} log - Logger object
28 | * @param {Object} context - Additional context (session)
29 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
30 | */
31 | export async function addTagDirect(args, log, context = {}) {
32 | // Destructure expected args
33 | const {
34 | tasksJsonPath,
35 | name,
36 | copyFromCurrent = false,
37 | copyFromTag,
38 | fromBranch = false,
39 | description,
40 | projectRoot
41 | } = args;
42 | const { session } = context;
43 |
44 | // Enable silent mode to prevent console logs from interfering with JSON response
45 | enableSilentMode();
46 |
47 | // Create logger wrapper using the utility
48 | const mcpLog = createLogWrapper(log);
49 |
50 | try {
51 | // Check if tasksJsonPath was provided
52 | if (!tasksJsonPath) {
53 | log.error('addTagDirect called without tasksJsonPath');
54 | disableSilentMode();
55 | return {
56 | success: false,
57 | error: {
58 | code: 'MISSING_ARGUMENT',
59 | message: 'tasksJsonPath is required'
60 | }
61 | };
62 | }
63 |
64 | // Handle --from-branch option
65 | if (fromBranch) {
66 | log.info('Creating tag from current git branch');
67 |
68 | // Import git utilities
69 | const gitUtils = await import(
70 | '../../../../scripts/modules/utils/git-utils.js'
71 | );
72 |
73 | // Check if we're in a git repository
74 | if (!(await gitUtils.isGitRepository(projectRoot))) {
75 | log.error('Not in a git repository');
76 | disableSilentMode();
77 | return {
78 | success: false,
79 | error: {
80 | code: 'NOT_GIT_REPO',
81 | message: 'Not in a git repository. Cannot use fromBranch option.'
82 | }
83 | };
84 | }
85 |
86 | // Get current git branch
87 | const currentBranch = await gitUtils.getCurrentBranch(projectRoot);
88 | if (!currentBranch) {
89 | log.error('Could not determine current git branch');
90 | disableSilentMode();
91 | return {
92 | success: false,
93 | error: {
94 | code: 'NO_CURRENT_BRANCH',
95 | message: 'Could not determine current git branch.'
96 | }
97 | };
98 | }
99 |
100 | // Prepare options for branch-based tag creation
101 | const branchOptions = {
102 | copyFromCurrent,
103 | copyFromTag,
104 | description:
105 | description || `Tag created from git branch "${currentBranch}"`
106 | };
107 |
108 | // Call the createTagFromBranch function
109 | const result = await createTagFromBranch(
110 | tasksJsonPath,
111 | currentBranch,
112 | branchOptions,
113 | {
114 | session,
115 | mcpLog,
116 | projectRoot
117 | },
118 | 'json' // outputFormat - use 'json' to suppress CLI UI
119 | );
120 |
121 | // Restore normal logging
122 | disableSilentMode();
123 |
124 | return {
125 | success: true,
126 | data: {
127 | branchName: result.branchName,
128 | tagName: result.tagName,
129 | created: result.created,
130 | mappingUpdated: result.mappingUpdated,
131 | message: `Successfully created tag "${result.tagName}" from git branch "${result.branchName}"`
132 | }
133 | };
134 | } else {
135 | // Check required parameters for regular tag creation
136 | if (!name || typeof name !== 'string') {
137 | log.error('Missing required parameter: name');
138 | disableSilentMode();
139 | return {
140 | success: false,
141 | error: {
142 | code: 'MISSING_PARAMETER',
143 | message: 'Tag name is required and must be a string'
144 | }
145 | };
146 | }
147 |
148 | log.info(`Creating new tag: ${name}`);
149 |
150 | // Prepare options
151 | const options = {
152 | copyFromCurrent,
153 | copyFromTag,
154 | description
155 | };
156 |
157 | // Call the createTag function
158 | const result = await createTag(
159 | tasksJsonPath,
160 | name,
161 | options,
162 | {
163 | session,
164 | mcpLog,
165 | projectRoot
166 | },
167 | 'json' // outputFormat - use 'json' to suppress CLI UI
168 | );
169 |
170 | // Restore normal logging
171 | disableSilentMode();
172 |
173 | return {
174 | success: true,
175 | data: {
176 | tagName: result.tagName,
177 | created: result.created,
178 | tasksCopied: result.tasksCopied,
179 | sourceTag: result.sourceTag,
180 | description: result.description,
181 | message: `Successfully created tag "${result.tagName}"`
182 | }
183 | };
184 | }
185 | } catch (error) {
186 | // Make sure to restore normal logging even if there's an error
187 | disableSilentMode();
188 |
189 | log.error(`Error in addTagDirect: ${error.message}`);
190 | return {
191 | success: false,
192 | error: {
193 | code: error.code || 'ADD_TAG_ERROR',
194 | message: error.message
195 | }
196 | };
197 | }
198 | }
199 |
```
--------------------------------------------------------------------------------
/apps/extension/src/webview/components/EmptyState.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import React from 'react';
2 | import { ExternalLink, Terminal, MessageSquare, Plus } from 'lucide-react';
3 | import { TaskMasterLogo } from '../../components/TaskMasterLogo';
4 |
5 | interface EmptyStateProps {
6 | currentTag: string;
7 | }
8 |
9 | export const EmptyState: React.FC<EmptyStateProps> = ({ currentTag }) => {
10 | return (
11 | <div className="flex items-center justify-center h-full overflow-auto">
12 | <div className="max-w-2xl mx-auto text-center p-8">
13 | {/* Empty state illustration */}
14 | <div className="mb-8 max-w-96 mx-auto">
15 | <TaskMasterLogo className="w-32 h-32 mx-auto text-vscode-foreground/20" />
16 | </div>
17 |
18 | <h2 className="text-2xl font-semibold mb-2 text-vscode-foreground">
19 | No tasks in "{currentTag}" tag
20 | </h2>
21 | <p className="text-vscode-foreground/70 mb-8">
22 | Get started by adding tasks to this tag using the commands below
23 | </p>
24 |
25 | {/* Command suggestions */}
26 | <div className="space-y-4 text-left">
27 | <div className="bg-vscode-editor-background/50 border border-vscode-panel-border rounded-lg p-4">
28 | <div className="flex items-center gap-2 mb-2">
29 | <Terminal className="w-4 h-4 text-vscode-terminal-ansiGreen" />
30 | <h3 className="font-medium">CLI Commands</h3>
31 | </div>
32 | <div className="space-y-2">
33 | <div className="bg-vscode-editor-background rounded p-2 font-mono text-sm">
34 | <span className="text-vscode-terminal-ansiYellow">
35 | task-master
36 | </span>{' '}
37 | <span className="text-vscode-terminal-ansiCyan">parse-prd</span>{' '}
38 | <span className="text-vscode-foreground/70">
39 | <path-to-prd>
40 | </span>{' '}
41 | <span className="text-vscode-terminal-ansiMagenta">
42 | --append
43 | </span>
44 | <div className="text-xs text-vscode-foreground/50 mt-1">
45 | Parse a PRD and append tasks to current tag
46 | </div>
47 | </div>
48 | <div className="bg-vscode-editor-background rounded p-2 font-mono text-sm">
49 | <span className="text-vscode-terminal-ansiYellow">
50 | task-master
51 | </span>{' '}
52 | <span className="text-vscode-terminal-ansiCyan">add-task</span>{' '}
53 | <span className="text-vscode-terminal-ansiMagenta">
54 | --prompt
55 | </span>{' '}
56 | <span className="text-vscode-foreground/70">
57 | "Your task description"
58 | </span>
59 | <div className="text-xs text-vscode-foreground/50 mt-1">
60 | Add a single task with AI assistance
61 | </div>
62 | </div>
63 | <div className="bg-vscode-editor-background rounded p-2 font-mono text-sm">
64 | <span className="text-vscode-terminal-ansiYellow">
65 | task-master
66 | </span>{' '}
67 | <span className="text-vscode-terminal-ansiCyan">add-task</span>{' '}
68 | <span className="text-vscode-terminal-ansiMagenta">--help</span>
69 | <div className="text-xs text-vscode-foreground/50 mt-1">
70 | View all options for adding tasks
71 | </div>
72 | </div>
73 | </div>
74 | </div>
75 |
76 | <div className="bg-vscode-editor-background/50 border border-vscode-panel-border rounded-lg p-4">
77 | <div className="flex items-center gap-2 mb-2">
78 | <MessageSquare className="w-4 h-4 text-vscode-textLink-foreground" />
79 | <h3 className="font-medium">MCP Examples</h3>
80 | </div>
81 | <div className="space-y-2 text-sm">
82 | <div className="flex items-start gap-2">
83 | <Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" />
84 | <div>
85 | <div className="text-vscode-foreground">
86 | "Add a task to tag {currentTag}: Implement user
87 | authentication"
88 | </div>
89 | </div>
90 | </div>
91 | <div className="flex items-start gap-2">
92 | <Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" />
93 | <div>
94 | <div className="text-vscode-foreground">
95 | "Parse this PRD and add tasks to {currentTag}: [paste PRD
96 | content]"
97 | </div>
98 | </div>
99 | </div>
100 | <div className="flex items-start gap-2">
101 | <Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" />
102 | <div>
103 | <div className="text-vscode-foreground">
104 | "Create 5 tasks for building a REST API in tag {currentTag}"
105 | </div>
106 | </div>
107 | </div>
108 | </div>
109 | </div>
110 |
111 | {/* Documentation link */}
112 | <div className="flex justify-center pt-4">
113 | <a
114 | href="https://docs.task-master.dev"
115 | className="inline-flex items-center gap-2 text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground transition-colors"
116 | onClick={(e) => {
117 | e.preventDefault();
118 | // Use VS Code API to open external link
119 | if (window.acquireVsCodeApi) {
120 | const vscode = window.acquireVsCodeApi();
121 | vscode.postMessage({
122 | type: 'openExternal',
123 | url: 'https://docs.task-master.dev'
124 | });
125 | }
126 | }}
127 | >
128 | <ExternalLink className="w-4 h-4" />
129 | <span className="text-sm font-medium">
130 | View TaskMaster Documentation
131 | </span>
132 | </a>
133 | </div>
134 | </div>
135 | </div>
136 | </div>
137 | );
138 | };
139 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/workflow/managers/workflow-state-manager.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Tests for WorkflowStateManager path sanitization
3 | */
4 |
5 | import os from 'node:os';
6 | import path from 'node:path';
7 | import { describe, expect, it } from 'vitest';
8 | import { WorkflowStateManager } from './workflow-state-manager.js';
9 |
10 | describe('WorkflowStateManager', () => {
11 | describe('getProjectIdentifier', () => {
12 | it('should sanitize paths like Claude Code', () => {
13 | const projectRoot =
14 | '/Volumes/Workspace/workspace/contrib/task-master/demos/nextjs-todo-tdd';
15 | const manager = new WorkflowStateManager(projectRoot);
16 |
17 | const sessionDir = manager.getSessionDir();
18 | const homeDir = os.homedir();
19 |
20 | // Expected structure: ~/.taskmaster/{project-id}/sessions/
21 | const expectedPath = path.join(
22 | homeDir,
23 | '.taskmaster',
24 | '-Volumes-Workspace-workspace-contrib-task-master-demos-nextjs-todo-tdd',
25 | 'sessions'
26 | );
27 |
28 | expect(sessionDir).toBe(expectedPath);
29 | });
30 |
31 | it('should preserve case in paths', () => {
32 | const projectRoot = '/Users/Alice/Projects/MyApp';
33 | const manager = new WorkflowStateManager(projectRoot);
34 |
35 | const sessionDir = manager.getSessionDir();
36 | // Extract project ID from: ~/.taskmaster/{project-id}/sessions/
37 | const projectId = sessionDir.split(path.sep).slice(-2, -1)[0];
38 |
39 | // Case should be preserved
40 | expect(projectId).toContain('Users');
41 | expect(projectId).toContain('Alice');
42 | expect(projectId).toContain('Projects');
43 | expect(projectId).toContain('MyApp');
44 | });
45 |
46 | it('should handle paths with special characters', () => {
47 | const projectRoot = '/tmp/my-project_v2.0/test';
48 | const manager = new WorkflowStateManager(projectRoot);
49 |
50 | const sessionDir = manager.getSessionDir();
51 | // Extract project ID from: ~/.taskmaster/{project-id}/sessions/
52 | const projectId = sessionDir.split(path.sep).slice(-2, -1)[0];
53 |
54 | // Special chars should be replaced with dashes
55 | expect(projectId).toBe('-tmp-my-project-v2-0-test');
56 | });
57 |
58 | it('should create unique identifiers for different paths', () => {
59 | const project1 = '/Users/alice/task-master';
60 | const project2 = '/Users/bob/task-master';
61 |
62 | const manager1 = new WorkflowStateManager(project1);
63 | const manager2 = new WorkflowStateManager(project2);
64 |
65 | // Extract project IDs from: ~/.taskmaster/{project-id}/sessions/
66 | const id1 = manager1.getSessionDir().split(path.sep).slice(-2, -1)[0];
67 | const id2 = manager2.getSessionDir().split(path.sep).slice(-2, -1)[0];
68 |
69 | // Same basename but different full paths should be unique
70 | expect(id1).not.toBe(id2);
71 | expect(id1).toContain('alice');
72 | expect(id2).toContain('bob');
73 | });
74 |
75 | it('should collapse multiple dashes', () => {
76 | const projectRoot = '/path//with///multiple////slashes';
77 | const manager = new WorkflowStateManager(projectRoot);
78 |
79 | const sessionDir = manager.getSessionDir();
80 | // Extract project ID from: ~/.taskmaster/{project-id}/sessions/
81 | const projectId = sessionDir.split(path.sep).slice(-2, -1)[0];
82 |
83 | // Multiple dashes should be collapsed to single dash
84 | expect(projectId).not.toContain('--');
85 | expect(projectId).toBe('-path-with-multiple-slashes');
86 | });
87 |
88 | it('should not have trailing dashes', () => {
89 | const projectRoot = '/path/to/project';
90 | const manager = new WorkflowStateManager(projectRoot);
91 |
92 | const sessionDir = manager.getSessionDir();
93 | // Extract project ID from: ~/.taskmaster/{project-id}/sessions/
94 | const projectId = sessionDir.split(path.sep).slice(-2, -1)[0];
95 |
96 | // Should not end with dash
97 | expect(projectId).not.toMatch(/-$/);
98 | });
99 |
100 | it('should start with a dash like Claude Code', () => {
101 | const projectRoot = '/any/path';
102 | const manager = new WorkflowStateManager(projectRoot);
103 |
104 | const sessionDir = manager.getSessionDir();
105 | // Extract project ID from: ~/.taskmaster/{project-id}/sessions/
106 | const projectId = sessionDir.split(path.sep).slice(-2, -1)[0];
107 |
108 | // Should start with dash like Claude Code's pattern
109 | expect(projectId).toMatch(/^-/);
110 | });
111 | });
112 |
113 | describe('session paths', () => {
114 | it('should place sessions in global ~/.taskmaster/{project-id}/sessions/', () => {
115 | const projectRoot = '/some/project';
116 | const manager = new WorkflowStateManager(projectRoot);
117 |
118 | const sessionDir = manager.getSessionDir();
119 | const homeDir = os.homedir();
120 |
121 | // Should be: ~/.taskmaster/{project-id}/sessions/
122 | expect(sessionDir).toContain(path.join(homeDir, '.taskmaster'));
123 | expect(sessionDir).toMatch(/\.taskmaster\/.*\/sessions$/);
124 | });
125 |
126 | it('should include workflow-state.json in session dir', () => {
127 | const projectRoot = '/some/project';
128 | const manager = new WorkflowStateManager(projectRoot);
129 |
130 | const statePath = manager.getStatePath();
131 | const sessionDir = manager.getSessionDir();
132 |
133 | expect(statePath).toBe(path.join(sessionDir, 'workflow-state.json'));
134 | });
135 |
136 | it('should include backups dir in session dir', () => {
137 | const projectRoot = '/some/project';
138 | const manager = new WorkflowStateManager(projectRoot);
139 |
140 | const backupDir = manager.getBackupDir();
141 | const sessionDir = manager.getSessionDir();
142 |
143 | expect(backupDir).toBe(path.join(sessionDir, 'backups'));
144 | });
145 | });
146 | });
147 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/git/services/template-engine.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * TemplateEngine - Configurable template system for generating text from templates
3 | *
4 | * Supports:
5 | * - Variable substitution using {{variableName}} syntax
6 | * - Custom templates via constructor or setTemplate
7 | * - Template validation with required variables
8 | * - Variable extraction from templates
9 | * - Multiple template storage and retrieval
10 | */
11 |
12 | export interface TemplateValidationResult {
13 | isValid: boolean;
14 | missingVars: string[];
15 | }
16 |
17 | export interface TemplateVariables {
18 | [key: string]: string | number | boolean | undefined;
19 | }
20 |
21 | export interface TemplateCollection {
22 | [templateName: string]: string;
23 | }
24 |
25 | export interface TemplateEngineOptions {
26 | customTemplates?: TemplateCollection;
27 | preservePlaceholders?: boolean;
28 | }
29 |
30 | const DEFAULT_TEMPLATES: TemplateCollection = {
31 | commitMessage: `{{type}}{{#scope}}({{scope}}){{/scope}}{{#breaking}}!{{/breaking}}: {{description}}
32 |
33 | {{#body}}{{body}}
34 |
35 | {{/body}}{{#taskId}}Task: {{taskId}}{{/taskId}}{{#phase}}
36 | Phase: {{phase}}{{/phase}}{{#testsPassing}}
37 | Tests: {{testsPassing}} passing{{#testsFailing}}, {{testsFailing}} failing{{/testsFailing}}{{/testsPassing}}`
38 | };
39 |
40 | export class TemplateEngine {
41 | private templates: TemplateCollection;
42 | private preservePlaceholders: boolean;
43 |
44 | constructor(
45 | optionsOrTemplates: TemplateEngineOptions | TemplateCollection = {}
46 | ) {
47 | // Backward compatibility: support old signature (TemplateCollection) and new signature (TemplateEngineOptions)
48 | const isOptions =
49 | 'customTemplates' in optionsOrTemplates ||
50 | 'preservePlaceholders' in optionsOrTemplates;
51 | const options: TemplateEngineOptions = isOptions
52 | ? (optionsOrTemplates as TemplateEngineOptions)
53 | : { customTemplates: optionsOrTemplates as TemplateCollection };
54 |
55 | this.templates = {
56 | ...DEFAULT_TEMPLATES,
57 | ...(options.customTemplates || {})
58 | };
59 | this.preservePlaceholders = options.preservePlaceholders ?? false;
60 | }
61 |
62 | /**
63 | * Render a template with provided variables
64 | */
65 | render(
66 | templateName: string,
67 | variables: TemplateVariables,
68 | inlineTemplate?: string
69 | ): string {
70 | const template =
71 | inlineTemplate !== undefined
72 | ? inlineTemplate
73 | : this.templates[templateName];
74 |
75 | if (template === undefined) {
76 | throw new Error(`Template "${templateName}" not found`);
77 | }
78 |
79 | return this.substituteVariables(template, variables);
80 | }
81 |
82 | /**
83 | * Set or update a template
84 | */
85 | setTemplate(name: string, template: string): void {
86 | this.templates[name] = template;
87 | }
88 |
89 | /**
90 | * Get a template by name
91 | */
92 | getTemplate(name: string): string | undefined {
93 | return this.templates[name];
94 | }
95 |
96 | /**
97 | * Check if a template exists
98 | */
99 | hasTemplate(name: string): boolean {
100 | return name in this.templates;
101 | }
102 |
103 | /**
104 | * Validate that a template contains all required variables
105 | */
106 | validateTemplate(
107 | template: string,
108 | requiredVars: string[]
109 | ): TemplateValidationResult {
110 | const templateVars = this.extractVariables(template);
111 | const missingVars = requiredVars.filter(
112 | (varName) => !templateVars.includes(varName)
113 | );
114 |
115 | return {
116 | isValid: missingVars.length === 0,
117 | missingVars
118 | };
119 | }
120 |
121 | /**
122 | * Extract all variable names from a template
123 | */
124 | extractVariables(template: string): string[] {
125 | const regex = /\{\{\s*([^}#/\s]+)\s*\}\}/g;
126 | const matches = template.matchAll(regex);
127 | const variables = new Set<string>();
128 |
129 | for (const match of matches) {
130 | variables.add(match[1]);
131 | }
132 |
133 | return Array.from(variables);
134 | }
135 |
136 | /**
137 | * Substitute variables in template
138 | * Supports both {{variable}} and {{#variable}}...{{/variable}} (conditional blocks)
139 | */
140 | private substituteVariables(
141 | template: string,
142 | variables: TemplateVariables
143 | ): string {
144 | let result = template;
145 |
146 | // Handle conditional blocks first ({{#var}}...{{/var}})
147 | result = this.processConditionalBlocks(result, variables);
148 |
149 | // Handle simple variable substitution ({{var}})
150 | result = result.replace(/\{\{\s*([^}#/\s]+)\s*\}\}/g, (_, varName) => {
151 | const value = variables[varName];
152 | return value !== undefined && value !== null
153 | ? String(value)
154 | : this.preservePlaceholders
155 | ? `{{${varName}}}`
156 | : '';
157 | });
158 |
159 | return result;
160 | }
161 |
162 | /**
163 | * Process conditional blocks in template
164 | * {{#variable}}content{{/variable}} - shows content only if variable is truthy
165 | * Processes innermost blocks first to handle nesting
166 | */
167 | private processConditionalBlocks(
168 | template: string,
169 | variables: TemplateVariables
170 | ): string {
171 | let result = template;
172 | let hasChanges = true;
173 |
174 | // Keep processing until no more conditional blocks are found
175 | while (hasChanges) {
176 | const before = result;
177 |
178 | // Find and replace innermost conditional blocks (non-greedy match)
179 | result = result.replace(
180 | /\{\{#([^}]+)\}\}((?:(?!\{\{#).)*?)\{\{\/\1\}\}/gs,
181 | (_, varName, content) => {
182 | const value = variables[varName.trim()];
183 |
184 | // Show content if variable is truthy (not undefined, null, false, or empty string)
185 | if (
186 | value !== undefined &&
187 | value !== null &&
188 | value !== false &&
189 | value !== ''
190 | ) {
191 | return content;
192 | }
193 |
194 | return '';
195 | }
196 | );
197 |
198 | hasChanges = result !== before;
199 | }
200 |
201 | return result;
202 | }
203 | }
204 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/roo-integration.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 | import fs from 'fs';
3 | import path from 'path';
4 | import os from 'os';
5 |
6 | // Mock external modules
7 | jest.mock('child_process', () => ({
8 | execSync: jest.fn()
9 | }));
10 |
11 | // Mock console methods
12 | jest.mock('console', () => ({
13 | log: jest.fn(),
14 | info: jest.fn(),
15 | warn: jest.fn(),
16 | error: jest.fn(),
17 | clear: jest.fn()
18 | }));
19 |
20 | describe('Roo Integration', () => {
21 | let tempDir;
22 |
23 | beforeEach(() => {
24 | jest.clearAllMocks();
25 |
26 | // Create a temporary directory for testing
27 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
28 |
29 | // Spy on fs methods
30 | jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
31 | jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
32 | if (filePath.toString().includes('.roomodes')) {
33 | return 'Existing roomodes content';
34 | }
35 | if (filePath.toString().includes('-rules')) {
36 | return 'Existing mode rules content';
37 | }
38 | return '{}';
39 | });
40 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
41 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
42 | });
43 |
44 | afterEach(() => {
45 | // Clean up the temporary directory
46 | try {
47 | fs.rmSync(tempDir, { recursive: true, force: true });
48 | } catch (err) {
49 | console.error(`Error cleaning up: ${err.message}`);
50 | }
51 | });
52 |
53 | // Test function that simulates the createProjectStructure behavior for Roo files
54 | function mockCreateRooStructure() {
55 | // Create main .roo directory
56 | fs.mkdirSync(path.join(tempDir, '.roo'), { recursive: true });
57 |
58 | // Create rules directory
59 | fs.mkdirSync(path.join(tempDir, '.roo', 'rules'), { recursive: true });
60 |
61 | // Create mode-specific rule directories
62 | const rooModes = [
63 | 'architect',
64 | 'ask',
65 | 'orchestrator',
66 | 'code',
67 | 'debug',
68 | 'test'
69 | ];
70 | for (const mode of rooModes) {
71 | fs.mkdirSync(path.join(tempDir, '.roo', `rules-${mode}`), {
72 | recursive: true
73 | });
74 | fs.writeFileSync(
75 | path.join(tempDir, '.roo', `rules-${mode}`, `${mode}-rules`),
76 | `Content for ${mode} rules`
77 | );
78 | }
79 |
80 | // Create additional directories
81 | fs.mkdirSync(path.join(tempDir, '.roo', 'config'), { recursive: true });
82 | fs.mkdirSync(path.join(tempDir, '.roo', 'templates'), { recursive: true });
83 | fs.mkdirSync(path.join(tempDir, '.roo', 'logs'), { recursive: true });
84 |
85 | // Copy .roomodes file
86 | fs.writeFileSync(path.join(tempDir, '.roomodes'), 'Roomodes file content');
87 | }
88 |
89 | test('creates all required .roo directories', () => {
90 | // Act
91 | mockCreateRooStructure();
92 |
93 | // Assert
94 | expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.roo'), {
95 | recursive: true
96 | });
97 | expect(fs.mkdirSync).toHaveBeenCalledWith(
98 | path.join(tempDir, '.roo', 'rules'),
99 | { recursive: true }
100 | );
101 |
102 | // Verify all mode directories are created
103 | expect(fs.mkdirSync).toHaveBeenCalledWith(
104 | path.join(tempDir, '.roo', 'rules-architect'),
105 | { recursive: true }
106 | );
107 | expect(fs.mkdirSync).toHaveBeenCalledWith(
108 | path.join(tempDir, '.roo', 'rules-ask'),
109 | { recursive: true }
110 | );
111 | expect(fs.mkdirSync).toHaveBeenCalledWith(
112 | path.join(tempDir, '.roo', 'rules-orchestrator'),
113 | { recursive: true }
114 | );
115 | expect(fs.mkdirSync).toHaveBeenCalledWith(
116 | path.join(tempDir, '.roo', 'rules-code'),
117 | { recursive: true }
118 | );
119 | expect(fs.mkdirSync).toHaveBeenCalledWith(
120 | path.join(tempDir, '.roo', 'rules-debug'),
121 | { recursive: true }
122 | );
123 | expect(fs.mkdirSync).toHaveBeenCalledWith(
124 | path.join(tempDir, '.roo', 'rules-test'),
125 | { recursive: true }
126 | );
127 | });
128 |
129 | test('creates rule files for all modes', () => {
130 | // Act
131 | mockCreateRooStructure();
132 |
133 | // Assert - check all rule files are created
134 | expect(fs.writeFileSync).toHaveBeenCalledWith(
135 | path.join(tempDir, '.roo', 'rules-architect', 'architect-rules'),
136 | expect.any(String)
137 | );
138 | expect(fs.writeFileSync).toHaveBeenCalledWith(
139 | path.join(tempDir, '.roo', 'rules-ask', 'ask-rules'),
140 | expect.any(String)
141 | );
142 | expect(fs.writeFileSync).toHaveBeenCalledWith(
143 | path.join(tempDir, '.roo', 'rules-orchestrator', 'orchestrator-rules'),
144 | expect.any(String)
145 | );
146 | expect(fs.writeFileSync).toHaveBeenCalledWith(
147 | path.join(tempDir, '.roo', 'rules-code', 'code-rules'),
148 | expect.any(String)
149 | );
150 | expect(fs.writeFileSync).toHaveBeenCalledWith(
151 | path.join(tempDir, '.roo', 'rules-debug', 'debug-rules'),
152 | expect.any(String)
153 | );
154 | expect(fs.writeFileSync).toHaveBeenCalledWith(
155 | path.join(tempDir, '.roo', 'rules-test', 'test-rules'),
156 | expect.any(String)
157 | );
158 | });
159 |
160 | test('creates .roomodes file in project root', () => {
161 | // Act
162 | mockCreateRooStructure();
163 |
164 | // Assert
165 | expect(fs.writeFileSync).toHaveBeenCalledWith(
166 | path.join(tempDir, '.roomodes'),
167 | expect.any(String)
168 | );
169 | });
170 |
171 | test('creates additional required Roo directories', () => {
172 | // Act
173 | mockCreateRooStructure();
174 |
175 | // Assert
176 | expect(fs.mkdirSync).toHaveBeenCalledWith(
177 | path.join(tempDir, '.roo', 'config'),
178 | { recursive: true }
179 | );
180 | expect(fs.mkdirSync).toHaveBeenCalledWith(
181 | path.join(tempDir, '.roo', 'templates'),
182 | { recursive: true }
183 | );
184 | expect(fs.mkdirSync).toHaveBeenCalledWith(
185 | path.join(tempDir, '.roo', 'logs'),
186 | { recursive: true }
187 | );
188 | });
189 | });
190 |
```
--------------------------------------------------------------------------------
/apps/extension/src/components/TaskDetails/DetailsSection.tsx:
--------------------------------------------------------------------------------
```typescript
1 | import type React from 'react';
2 | import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
3 |
4 | interface MarkdownRendererProps {
5 | content: string;
6 | className?: string;
7 | }
8 |
9 | const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
10 | content,
11 | className = ''
12 | }) => {
13 | const parseMarkdown = (text: string) => {
14 | const parts = [];
15 | const lines = text.split('\n');
16 | let currentBlock = [];
17 | let inCodeBlock = false;
18 | let codeLanguage = '';
19 |
20 | for (let i = 0; i < lines.length; i++) {
21 | const line = lines[i];
22 |
23 | if (line.startsWith('```')) {
24 | if (inCodeBlock) {
25 | if (currentBlock.length > 0) {
26 | parts.push({
27 | type: 'code',
28 | content: currentBlock.join('\n'),
29 | language: codeLanguage
30 | });
31 | currentBlock = [];
32 | }
33 | inCodeBlock = false;
34 | codeLanguage = '';
35 | } else {
36 | if (currentBlock.length > 0) {
37 | parts.push({
38 | type: 'text',
39 | content: currentBlock.join('\n')
40 | });
41 | currentBlock = [];
42 | }
43 | inCodeBlock = true;
44 | codeLanguage = line.substring(3).trim();
45 | }
46 | } else {
47 | currentBlock.push(line);
48 | }
49 | }
50 |
51 | if (currentBlock.length > 0) {
52 | parts.push({
53 | type: inCodeBlock ? 'code' : 'text',
54 | content: currentBlock.join('\n'),
55 | language: codeLanguage
56 | });
57 | }
58 |
59 | return parts;
60 | };
61 |
62 | const parts = parseMarkdown(content);
63 |
64 | return (
65 | <div className={className}>
66 | {parts.map((part, index) => {
67 | if (part.type === 'code') {
68 | return (
69 | <pre
70 | key={index}
71 | className="bg-vscode-editor-background rounded-md p-4 overflow-x-auto mb-4 border border-vscode-editor-lineHighlightBorder"
72 | >
73 | <code className="text-sm text-vscode-editor-foreground font-mono">
74 | {part.content}
75 | </code>
76 | </pre>
77 | );
78 | }
79 | return (
80 | <div key={index} className="whitespace-pre-wrap mb-4 last:mb-0">
81 | {part.content.split('\n').map((line, lineIndex) => {
82 | const bulletMatch = line.match(/^(\s*)([-*])\s(.+)$/);
83 | if (bulletMatch) {
84 | const indent = bulletMatch[1].length;
85 | return (
86 | <div
87 | key={lineIndex}
88 | className="flex gap-2 mb-1"
89 | style={{ paddingLeft: `${indent * 16}px` }}
90 | >
91 | <span className="text-vscode-foreground/60">•</span>
92 | <span className="flex-1">{bulletMatch[3]}</span>
93 | </div>
94 | );
95 | }
96 |
97 | const numberedMatch = line.match(/^(\s*)(\d+\.)\s(.+)$/);
98 | if (numberedMatch) {
99 | const indent = numberedMatch[1].length;
100 | return (
101 | <div
102 | key={lineIndex}
103 | className="flex gap-2 mb-1"
104 | style={{ paddingLeft: `${indent * 16}px` }}
105 | >
106 | <span className="text-vscode-foreground/60 font-mono">
107 | {numberedMatch[2]}
108 | </span>
109 | <span className="flex-1">{numberedMatch[3]}</span>
110 | </div>
111 | );
112 | }
113 |
114 | const headingMatch = line.match(/^(#{1,6})\s(.+)$/);
115 | if (headingMatch) {
116 | const level = headingMatch[1].length;
117 | const headingLevel = Math.min(level + 2, 6);
118 | const headingClassName =
119 | 'font-semibold text-vscode-foreground mb-2 mt-4 first:mt-0';
120 |
121 | switch (headingLevel) {
122 | case 3:
123 | return (
124 | <h3 key={lineIndex} className={headingClassName}>
125 | {headingMatch[2]}
126 | </h3>
127 | );
128 | case 4:
129 | return (
130 | <h4 key={lineIndex} className={headingClassName}>
131 | {headingMatch[2]}
132 | </h4>
133 | );
134 | case 5:
135 | return (
136 | <h5 key={lineIndex} className={headingClassName}>
137 | {headingMatch[2]}
138 | </h5>
139 | );
140 | case 6:
141 | return (
142 | <h6 key={lineIndex} className={headingClassName}>
143 | {headingMatch[2]}
144 | </h6>
145 | );
146 | default:
147 | return (
148 | <h3 key={lineIndex} className={headingClassName}>
149 | {headingMatch[2]}
150 | </h3>
151 | );
152 | }
153 | }
154 |
155 | if (line.trim() === '') {
156 | return <div key={lineIndex} className="h-2" />;
157 | }
158 |
159 | return (
160 | <div key={lineIndex} className="mb-2 last:mb-0">
161 | {line}
162 | </div>
163 | );
164 | })}
165 | </div>
166 | );
167 | })}
168 | </div>
169 | );
170 | };
171 |
172 | interface DetailsSectionProps {
173 | title: string;
174 | content?: string;
175 | error?: string | null;
176 | emptyMessage?: string;
177 | defaultExpanded?: boolean;
178 | }
179 |
180 | export const DetailsSection: React.FC<DetailsSectionProps> = ({
181 | title,
182 | content,
183 | error,
184 | emptyMessage = 'No details available',
185 | defaultExpanded = false
186 | }) => {
187 | return (
188 | <CollapsibleSection title={title} defaultExpanded={defaultExpanded}>
189 | <div className={title.toLowerCase().replace(/\s+/g, '-') + '-content'}>
190 | {error ? (
191 | <div className="text-sm text-red-400 py-2">
192 | Error loading {title.toLowerCase()}: {error}
193 | </div>
194 | ) : content !== undefined && content !== '' ? (
195 | <MarkdownRenderer content={content} />
196 | ) : (
197 | <div className="text-sm text-vscode-foreground/50 py-2">
198 | {emptyMessage}
199 | </div>
200 | )}
201 | </div>
202 | </CollapsibleSection>
203 | );
204 | };
205 |
```
--------------------------------------------------------------------------------
/src/utils/timeout-manager.js:
--------------------------------------------------------------------------------
```javascript
1 | import { StreamingError, STREAMING_ERROR_CODES } from './stream-parser.js';
2 |
3 | /**
4 | * Utility class for managing timeouts in async operations
5 | * Reduces code duplication for timeout handling patterns
6 | */
7 | export class TimeoutManager {
8 | /**
9 | * Wraps a promise with a timeout that will reject if not resolved in time
10 | *
11 | * @param {Promise} promise - The promise to wrap with timeout
12 | * @param {number} timeoutMs - Timeout duration in milliseconds
13 | * @param {string} operationName - Name of the operation for error messages
14 | * @returns {Promise} The result of the promise or throws timeout error
15 | *
16 | * @example
17 | * const result = await TimeoutManager.withTimeout(
18 | * fetchData(),
19 | * 5000,
20 | * 'Data fetch operation'
21 | * );
22 | */
23 | static async withTimeout(promise, timeoutMs, operationName = 'Operation') {
24 | let timeoutHandle;
25 |
26 | const timeoutPromise = new Promise((_, reject) => {
27 | timeoutHandle = setTimeout(() => {
28 | reject(
29 | new StreamingError(
30 | `${operationName} timed out after ${timeoutMs / 1000} seconds`,
31 | STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED
32 | )
33 | );
34 | }, timeoutMs);
35 | });
36 |
37 | try {
38 | // Race between the actual promise and the timeout
39 | const result = await Promise.race([promise, timeoutPromise]);
40 | // Clear timeout if promise resolved first
41 | clearTimeout(timeoutHandle);
42 | return result;
43 | } catch (error) {
44 | // Always clear timeout on error
45 | clearTimeout(timeoutHandle);
46 | throw error;
47 | }
48 | }
49 |
50 | /**
51 | * Wraps a promise with a timeout, but returns undefined instead of throwing on timeout
52 | * Useful for optional operations that shouldn't fail the main flow
53 | *
54 | * @param {Promise} promise - The promise to wrap with timeout
55 | * @param {number} timeoutMs - Timeout duration in milliseconds
56 | * @param {*} defaultValue - Value to return on timeout (default: undefined)
57 | * @returns {Promise} The result of the promise or defaultValue on timeout
58 | *
59 | * @example
60 | * const usage = await TimeoutManager.withSoftTimeout(
61 | * getUsageStats(),
62 | * 1000,
63 | * { tokens: 0 }
64 | * );
65 | */
66 | static async withSoftTimeout(promise, timeoutMs, defaultValue = undefined) {
67 | let timeoutHandle;
68 |
69 | const timeoutPromise = new Promise((resolve) => {
70 | timeoutHandle = setTimeout(() => {
71 | resolve(defaultValue);
72 | }, timeoutMs);
73 | });
74 |
75 | try {
76 | const result = await Promise.race([promise, timeoutPromise]);
77 | clearTimeout(timeoutHandle);
78 | return result;
79 | } catch (error) {
80 | // On error, clear timeout and return default value
81 | clearTimeout(timeoutHandle);
82 | return defaultValue;
83 | }
84 | }
85 |
86 | /**
87 | * Creates a reusable timeout controller for multiple operations
88 | * Useful when you need to apply the same timeout to multiple promises
89 | *
90 | * @param {number} timeoutMs - Timeout duration in milliseconds
91 | * @param {string} operationName - Base name for operations
92 | * @returns {Object} Controller with wrap method
93 | *
94 | * @example
95 | * const controller = TimeoutManager.createController(60000, 'AI Service');
96 | * const result1 = await controller.wrap(service.call1(), 'call 1');
97 | * const result2 = await controller.wrap(service.call2(), 'call 2');
98 | */
99 | static createController(timeoutMs, operationName = 'Operation') {
100 | return {
101 | timeoutMs,
102 | operationName,
103 |
104 | async wrap(promise, specificName = null) {
105 | const fullName = specificName
106 | ? `${operationName} - ${specificName}`
107 | : operationName;
108 | return TimeoutManager.withTimeout(promise, timeoutMs, fullName);
109 | },
110 |
111 | async wrapSoft(promise, defaultValue = undefined) {
112 | return TimeoutManager.withSoftTimeout(promise, timeoutMs, defaultValue);
113 | }
114 | };
115 | }
116 |
117 | /**
118 | * Checks if an error is a timeout error from this manager
119 | *
120 | * @param {Error} error - The error to check
121 | * @returns {boolean} True if this is a timeout error
122 | */
123 | static isTimeoutError(error) {
124 | return (
125 | error instanceof StreamingError &&
126 | error.code === STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED &&
127 | error.message.includes('timed out')
128 | );
129 | }
130 | }
131 |
132 | /**
133 | * Duration helper class for more readable timeout specifications
134 | */
135 | export class Duration {
136 | constructor(value, unit = 'ms') {
137 | this.milliseconds = this._toMilliseconds(value, unit);
138 | }
139 |
140 | static milliseconds(value) {
141 | return new Duration(value, 'ms');
142 | }
143 |
144 | static seconds(value) {
145 | return new Duration(value, 's');
146 | }
147 |
148 | static minutes(value) {
149 | return new Duration(value, 'm');
150 | }
151 |
152 | static hours(value) {
153 | return new Duration(value, 'h');
154 | }
155 |
156 | get seconds() {
157 | return this.milliseconds / 1000;
158 | }
159 |
160 | get minutes() {
161 | return this.milliseconds / 60000;
162 | }
163 |
164 | get hours() {
165 | return this.milliseconds / 3600000;
166 | }
167 |
168 | toString() {
169 | if (this.milliseconds < 1000) {
170 | return `${this.milliseconds}ms`;
171 | } else if (this.milliseconds < 60000) {
172 | return `${this.seconds}s`;
173 | } else if (this.milliseconds < 3600000) {
174 | return `${Math.floor(this.minutes)}m ${Math.floor(this.seconds % 60)}s`;
175 | } else {
176 | return `${Math.floor(this.hours)}h ${Math.floor(this.minutes % 60)}m`;
177 | }
178 | }
179 |
180 | _toMilliseconds(value, unit) {
181 | const conversions = {
182 | ms: 1,
183 | s: 1000,
184 | m: 60000,
185 | h: 3600000
186 | };
187 | return value * (conversions[unit] || 1);
188 | }
189 | }
190 |
```
--------------------------------------------------------------------------------
/packages/ai-sdk-provider-grok-cli/src/errors.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tests for error handling utilities
3 | */
4 |
5 | import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider';
6 | import { describe, expect, it } from 'vitest';
7 | import {
8 | createAPICallError,
9 | createAuthenticationError,
10 | createInstallationError,
11 | createTimeoutError,
12 | getErrorMetadata,
13 | isAuthenticationError,
14 | isInstallationError,
15 | isTimeoutError
16 | } from './errors.js';
17 |
18 | describe('createAPICallError', () => {
19 | it('should create APICallError with metadata', () => {
20 | const error = createAPICallError({
21 | message: 'Test error',
22 | code: 'TEST_ERROR',
23 | exitCode: 1,
24 | stderr: 'Error output',
25 | stdout: 'Success output',
26 | promptExcerpt: 'Test prompt',
27 | isRetryable: true
28 | });
29 |
30 | expect(error).toBeInstanceOf(APICallError);
31 | expect(error.message).toBe('Test error');
32 | expect(error.isRetryable).toBe(true);
33 | expect(error.url).toBe('grok-cli://command');
34 | expect(error.data).toEqual({
35 | code: 'TEST_ERROR',
36 | exitCode: 1,
37 | stderr: 'Error output',
38 | stdout: 'Success output',
39 | promptExcerpt: 'Test prompt'
40 | });
41 | });
42 |
43 | it('should create APICallError with minimal parameters', () => {
44 | const error = createAPICallError({
45 | message: 'Simple error'
46 | });
47 |
48 | expect(error).toBeInstanceOf(APICallError);
49 | expect(error.message).toBe('Simple error');
50 | expect(error.isRetryable).toBe(false);
51 | });
52 | });
53 |
54 | describe('createAuthenticationError', () => {
55 | it('should create LoadAPIKeyError with custom message', () => {
56 | const error = createAuthenticationError({
57 | message: 'Custom auth error'
58 | });
59 |
60 | expect(error).toBeInstanceOf(LoadAPIKeyError);
61 | expect(error.message).toBe('Custom auth error');
62 | });
63 |
64 | it('should create LoadAPIKeyError with default message', () => {
65 | const error = createAuthenticationError({});
66 |
67 | expect(error).toBeInstanceOf(LoadAPIKeyError);
68 | expect(error.message).toContain('Authentication failed');
69 | });
70 | });
71 |
72 | describe('createTimeoutError', () => {
73 | it('should create APICallError for timeout', () => {
74 | const error = createTimeoutError({
75 | message: 'Operation timed out',
76 | timeoutMs: 5000,
77 | promptExcerpt: 'Test prompt'
78 | });
79 |
80 | expect(error).toBeInstanceOf(APICallError);
81 | expect(error.message).toBe('Operation timed out');
82 | expect(error.isRetryable).toBe(true);
83 | expect(error.data).toEqual({
84 | code: 'TIMEOUT',
85 | promptExcerpt: 'Test prompt',
86 | timeoutMs: 5000
87 | });
88 | });
89 | });
90 |
91 | describe('createInstallationError', () => {
92 | it('should create APICallError for installation issues', () => {
93 | const error = createInstallationError({
94 | message: 'CLI not found'
95 | });
96 |
97 | expect(error).toBeInstanceOf(APICallError);
98 | expect(error.message).toBe('CLI not found');
99 | expect(error.isRetryable).toBe(false);
100 | expect(error.url).toBe('grok-cli://installation');
101 | });
102 |
103 | it('should create APICallError with default message', () => {
104 | const error = createInstallationError({});
105 |
106 | expect(error).toBeInstanceOf(APICallError);
107 | expect(error.message).toContain('Grok CLI is not installed');
108 | });
109 | });
110 |
111 | describe('isAuthenticationError', () => {
112 | it('should return true for LoadAPIKeyError', () => {
113 | const error = new LoadAPIKeyError({ message: 'Auth failed' });
114 | expect(isAuthenticationError(error)).toBe(true);
115 | });
116 |
117 | it('should return true for APICallError with 401 exit code', () => {
118 | const error = new APICallError({
119 | message: 'Unauthorized',
120 | data: { exitCode: 401 }
121 | });
122 | expect(isAuthenticationError(error)).toBe(true);
123 | });
124 |
125 | it('should return false for other errors', () => {
126 | const error = new Error('Generic error');
127 | expect(isAuthenticationError(error)).toBe(false);
128 | });
129 | });
130 |
131 | describe('isTimeoutError', () => {
132 | it('should return true for timeout APICallError', () => {
133 | const error = new APICallError({
134 | message: 'Timeout',
135 | data: { code: 'TIMEOUT' }
136 | });
137 | expect(isTimeoutError(error)).toBe(true);
138 | });
139 |
140 | it('should return false for other errors', () => {
141 | const error = new APICallError({ message: 'Other error' });
142 | expect(isTimeoutError(error)).toBe(false);
143 | });
144 | });
145 |
146 | describe('isInstallationError', () => {
147 | it('should return true for installation APICallError', () => {
148 | const error = new APICallError({
149 | message: 'Not installed',
150 | url: 'grok-cli://installation'
151 | });
152 | expect(isInstallationError(error)).toBe(true);
153 | });
154 |
155 | it('should return false for other errors', () => {
156 | const error = new APICallError({ message: 'Other error' });
157 | expect(isInstallationError(error)).toBe(false);
158 | });
159 | });
160 |
161 | describe('getErrorMetadata', () => {
162 | it('should return metadata from APICallError', () => {
163 | const metadata = {
164 | code: 'TEST_ERROR',
165 | exitCode: 1,
166 | stderr: 'Error output'
167 | };
168 | const error = new APICallError({
169 | message: 'Test error',
170 | data: metadata
171 | });
172 |
173 | const result = getErrorMetadata(error);
174 | expect(result).toEqual(metadata);
175 | });
176 |
177 | it('should return undefined for errors without metadata', () => {
178 | const error = new Error('Generic error');
179 | const result = getErrorMetadata(error);
180 | expect(result).toBeUndefined();
181 | });
182 |
183 | it('should return undefined for APICallError without data', () => {
184 | const error = new APICallError({ message: 'Test error' });
185 | const result = getErrorMetadata(error);
186 | expect(result).toBeUndefined();
187 | });
188 | });
189 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/git/services/scope-detector.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * ScopeDetector - Intelligent scope detection from changed files
3 | *
4 | * Automatically determines conventional commit scopes based on file paths
5 | * using configurable pattern matching and priority-based resolution.
6 | * // TODO: remove this
7 | */
8 |
9 | export interface ScopeMapping {
10 | [pattern: string]: string;
11 | }
12 |
13 | export interface ScopePriority {
14 | [scope: string]: number;
15 | }
16 |
17 | // Ordered from most specific to least specific
18 | const DEFAULT_SCOPE_MAPPINGS: Array<[string, string]> = [
19 | // Special file types (check first - most specific)
20 | ['**/*.test.*', 'test'],
21 | ['**/*.spec.*', 'test'],
22 | ['**/test/**', 'test'],
23 | ['**/tests/**', 'test'],
24 | ['**/__tests__/**', 'test'],
25 |
26 | // Dependencies (specific files)
27 | ['**/package-lock.json', 'deps'],
28 | ['package-lock.json', 'deps'],
29 | ['**/pnpm-lock.yaml', 'deps'],
30 | ['pnpm-lock.yaml', 'deps'],
31 | ['**/yarn.lock', 'deps'],
32 | ['yarn.lock', 'deps'],
33 |
34 | // Configuration files (before packages so root configs don't match package patterns)
35 | ['**/package.json', 'config'],
36 | ['package.json', 'config'],
37 | ['**/tsconfig*.json', 'config'],
38 | ['tsconfig*.json', 'config'],
39 | ['**/.eslintrc*', 'config'],
40 | ['.eslintrc*', 'config'],
41 | ['**/vite.config.*', 'config'],
42 | ['vite.config.*', 'config'],
43 | ['**/vitest.config.*', 'config'],
44 | ['vitest.config.*', 'config'],
45 |
46 | // Package-level scopes (more specific than feature-level)
47 | ['packages/cli/**', 'cli'],
48 | ['packages/tm-core/**', 'core'],
49 | ['packages/mcp-server/**', 'mcp'],
50 |
51 | // Feature-level scopes (within any package)
52 | ['**/workflow/**', 'workflow'],
53 | ['**/git/**', 'git'],
54 | ['**/storage/**', 'storage'],
55 | ['**/auth/**', 'auth'],
56 | ['**/config/**', 'config'],
57 |
58 | // Documentation (least specific)
59 | ['**/*.md', 'docs'],
60 | ['**/docs/**', 'docs'],
61 | ['README*', 'docs'],
62 | ['CHANGELOG*', 'docs']
63 | ];
64 |
65 | const DEFAULT_SCOPE_PRIORITIES: ScopePriority = {
66 | core: 100,
67 | cli: 90,
68 | mcp: 85,
69 | workflow: 80,
70 | git: 75,
71 | storage: 70,
72 | auth: 65,
73 | config: 60,
74 | test: 50,
75 | docs: 30,
76 | deps: 20,
77 | repo: 10
78 | };
79 |
80 | export class ScopeDetector {
81 | private scopeMappings: Array<[string, string]>;
82 | private scopePriorities: ScopePriority;
83 |
84 | constructor(customMappings?: ScopeMapping, customPriorities?: ScopePriority) {
85 | // Start with default mappings
86 | this.scopeMappings = [...DEFAULT_SCOPE_MAPPINGS];
87 |
88 | // Add custom mappings at the start (highest priority)
89 | if (customMappings) {
90 | const customEntries = Object.entries(customMappings);
91 | this.scopeMappings = [...customEntries, ...this.scopeMappings];
92 | }
93 |
94 | this.scopePriorities = {
95 | ...DEFAULT_SCOPE_PRIORITIES,
96 | ...customPriorities
97 | };
98 | }
99 |
100 | /**
101 | * Detect the most relevant scope from a list of changed files
102 | * Returns the scope with the highest priority
103 | */
104 | detectScope(files: string[]): string {
105 | if (files.length === 0) {
106 | return 'repo';
107 | }
108 |
109 | const scopeCounts = new Map<string, number>();
110 |
111 | // Count occurrences of each scope
112 | for (const file of files) {
113 | const scope = this.getMatchingScope(file);
114 | if (scope) {
115 | scopeCounts.set(scope, (scopeCounts.get(scope) || 0) + 1);
116 | }
117 | }
118 |
119 | // If no scopes matched, default to 'repo'
120 | if (scopeCounts.size === 0) {
121 | return 'repo';
122 | }
123 |
124 | // Find scope with highest priority (considering both priority and count)
125 | let bestScope = 'repo';
126 | let bestScore = 0;
127 |
128 | for (const [scope, count] of scopeCounts) {
129 | const priority = this.getScopePriority(scope);
130 | // Score = priority * count (files in that scope)
131 | const score = priority * count;
132 |
133 | if (score > bestScore) {
134 | bestScore = score;
135 | bestScope = scope;
136 | }
137 | }
138 |
139 | return bestScope;
140 | }
141 |
142 | /**
143 | * Get all matching scopes for the given files
144 | */
145 | getAllMatchingScopes(files: string[]): string[] {
146 | const scopes = new Set<string>();
147 |
148 | for (const file of files) {
149 | const scope = this.getMatchingScope(file);
150 | if (scope) {
151 | scopes.add(scope);
152 | }
153 | }
154 |
155 | return Array.from(scopes);
156 | }
157 |
158 | /**
159 | * Get the matching scope for a single file
160 | * Returns the first matching scope (order matters!)
161 | */
162 | getMatchingScope(file: string): string | null {
163 | // Normalize path separators
164 | const normalizedFile = file.replace(/\\/g, '/');
165 |
166 | for (const [pattern, scope] of this.scopeMappings) {
167 | if (this.matchesPattern(normalizedFile, pattern)) {
168 | return scope;
169 | }
170 | }
171 |
172 | return null;
173 | }
174 |
175 | /**
176 | * Get the priority of a scope
177 | */
178 | getScopePriority(scope: string): number {
179 | return this.scopePriorities[scope] || 0;
180 | }
181 |
182 | /**
183 | * Match a file path against a glob-like pattern
184 | * Supports:
185 | * - ** for multi-level directory matching
186 | * - * for single-level matching
187 | */
188 | private matchesPattern(filePath: string, pattern: string): boolean {
189 | // Replace ** first with a unique placeholder
190 | let regexPattern = pattern.replace(/\*\*/g, '§GLOBSTAR§');
191 |
192 | // Escape special regex characters (but not our placeholder or *)
193 | regexPattern = regexPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
194 |
195 | // Replace single * with [^/]* (matches anything except /)
196 | regexPattern = regexPattern.replace(/\*/g, '[^/]*');
197 |
198 | // Replace placeholder with .* (matches anything including /)
199 | regexPattern = regexPattern.replace(/§GLOBSTAR§/g, '.*');
200 |
201 | const regex = new RegExp(`^${regexPattern}$`);
202 | return regex.test(filePath);
203 | }
204 | }
205 |
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "task-master-ai",
3 | "version": "0.33.0",
4 | "description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
5 | "main": "index.js",
6 | "type": "module",
7 | "bin": {
8 | "task-master": "dist/task-master.js",
9 | "task-master-mcp": "dist/mcp-server.js",
10 | "task-master-ai": "dist/mcp-server.js"
11 | },
12 | "workspaces": ["apps/*", "packages/*", "."],
13 | "scripts": {
14 | "build": "npm run build:build-config && cross-env NODE_ENV=production tsdown",
15 | "dev": "tsdown --watch",
16 | "turbo:dev": "turbo dev",
17 | "turbo:build": "turbo build",
18 | "turbo:typecheck": "turbo typecheck",
19 | "build:build-config": "npm run build -w @tm/build-config",
20 | "test": "cross-env NODE_ENV=test node --experimental-vm-modules node_modules/.bin/jest",
21 | "test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit",
22 | "test:integration": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=integration",
23 | "test:fails": "node --experimental-vm-modules node_modules/.bin/jest --onlyFailures",
24 | "test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch",
25 | "test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage",
26 | "test:ci": "node --experimental-vm-modules node_modules/.bin/jest --coverage --ci",
27 | "test:e2e": "./tests/e2e/run_e2e.sh",
28 | "test:e2e-report": "./tests/e2e/run_e2e.sh --analyze-log",
29 | "postpack": "chmod +x dist/task-master.js dist/mcp-server.js",
30 | "changeset": "changeset",
31 | "changeset:validate": "node .github/scripts/validate-changesets.mjs",
32 | "release": "changeset publish",
33 | "publish-packages": "turbo run build lint test && changeset version && changeset publish",
34 | "inspector": "npx @modelcontextprotocol/inspector node dist/mcp-server.js",
35 | "mcp-server": "node dist/mcp-server.js",
36 | "format-check": "biome format .",
37 | "format": "biome format . --write",
38 | "deps:check": "manypkg check || echo 'Note: Workspace package version warnings are expected for internal @tm/* packages'",
39 | "deps:fix": "manypkg fix"
40 | },
41 | "keywords": [
42 | "claude",
43 | "task",
44 | "management",
45 | "ai",
46 | "development",
47 | "cursor",
48 | "anthropic",
49 | "llm",
50 | "mcp",
51 | "context"
52 | ],
53 | "author": "Eyal Toledano",
54 | "license": "MIT WITH Commons-Clause",
55 | "dependencies": {
56 | "@ai-sdk/amazon-bedrock": "^3.0.23",
57 | "@ai-sdk/anthropic": "^2.0.18",
58 | "@ai-sdk/azure": "^2.0.34",
59 | "@ai-sdk/google": "^2.0.16",
60 | "@ai-sdk/google-vertex": "^3.0.29",
61 | "@ai-sdk/groq": "^2.0.21",
62 | "@ai-sdk/mistral": "^2.0.16",
63 | "@ai-sdk/openai": "^2.0.34",
64 | "@ai-sdk/openai-compatible": "^1.0.25",
65 | "@ai-sdk/perplexity": "^2.0.10",
66 | "@ai-sdk/provider": "^2.0.0",
67 | "@ai-sdk/provider-utils": "^3.0.10",
68 | "@ai-sdk/xai": "^2.0.22",
69 | "@aws-sdk/credential-providers": "^3.895.0",
70 | "@inquirer/search": "^3.0.15",
71 | "@openrouter/ai-sdk-provider": "^1.2.0",
72 | "@streamparser/json": "^0.0.22",
73 | "@supabase/supabase-js": "^2.57.4",
74 | "ai": "^5.0.51",
75 | "ai-sdk-provider-claude-code": "^2.1.0",
76 | "ai-sdk-provider-codex-cli": "^0.3.0",
77 | "ai-sdk-provider-gemini-cli": "^1.1.1",
78 | "ajv": "^8.17.1",
79 | "ajv-formats": "^3.0.1",
80 | "boxen": "^8.0.1",
81 | "chalk": "5.6.2",
82 | "cli-highlight": "^2.1.11",
83 | "cli-progress": "^3.12.0",
84 | "cli-table3": "^0.6.5",
85 | "commander": "^12.1.0",
86 | "cors": "^2.8.5",
87 | "date-fns": "^4.1.0",
88 | "dotenv": "^16.6.1",
89 | "express": "^4.21.2",
90 | "fastmcp": "^3.23.1",
91 | "figlet": "^1.8.0",
92 | "fs-extra": "^11.3.0",
93 | "fuse.js": "^7.1.0",
94 | "gpt-tokens": "^1.3.14",
95 | "gradient-string": "^3.0.0",
96 | "helmet": "^8.1.0",
97 | "inquirer": "^12.5.0",
98 | "jsonc-parser": "^3.3.1",
99 | "jsonrepair": "^3.13.0",
100 | "jsonwebtoken": "^9.0.2",
101 | "lru-cache": "^10.2.0",
102 | "marked": "^15.0.12",
103 | "marked-terminal": "^7.3.0",
104 | "ollama-ai-provider-v2": "^1.3.1",
105 | "open": "^10.2.0",
106 | "ora": "^8.2.0",
107 | "simple-git": "^3.28.0",
108 | "steno": "^4.0.2",
109 | "undici": "^7.16.0",
110 | "uuid": "^11.1.0",
111 | "zod": "^4.1.12"
112 | },
113 | "optionalDependencies": {
114 | "@anthropic-ai/claude-code": "^1.0.88",
115 | "@biomejs/cli-linux-x64": "^1.9.4"
116 | },
117 | "engines": {
118 | "node": ">=18.0.0"
119 | },
120 | "packageManager": "[email protected]",
121 | "repository": {
122 | "type": "git",
123 | "url": "git+https://github.com/eyaltoledano/claude-task-master.git"
124 | },
125 | "homepage": "https://github.com/eyaltoledano/claude-task-master#readme",
126 | "bugs": {
127 | "url": "https://github.com/eyaltoledano/claude-task-master/issues"
128 | },
129 | "files": [
130 | "dist/**",
131 | "README-task-master.md",
132 | "README.md",
133 | "LICENSE",
134 | "CHANGELOG.md"
135 | ],
136 | "overrides": {
137 | "node-fetch": "^2.6.12",
138 | "whatwg-url": "^11.0.0"
139 | },
140 | "devDependencies": {
141 | "@biomejs/biome": "^1.9.4",
142 | "@changesets/changelog-github": "^0.5.1",
143 | "@changesets/cli": "^2.28.1",
144 | "@manypkg/cli": "^0.25.1",
145 | "@tm/ai-sdk-provider-grok-cli": "*",
146 | "@tm/cli": "*",
147 | "@types/fs-extra": "^11.0.4",
148 | "@types/jest": "^29.5.14",
149 | "@types/marked-terminal": "^6.1.1",
150 | "concurrently": "^9.2.1",
151 | "cross-env": "^10.0.0",
152 | "execa": "^8.0.1",
153 | "jest": "^29.7.0",
154 | "jest-environment-node": "^29.7.0",
155 | "mock-fs": "^5.5.0",
156 | "prettier": "^3.5.3",
157 | "supertest": "^7.1.0",
158 | "ts-jest": "^29.4.2",
159 | "tsdown": "^0.15.2",
160 | "tsx": "^4.20.4",
161 | "turbo": "2.5.6",
162 | "typescript": "^5.9.2"
163 | }
164 | }
165 |
```
--------------------------------------------------------------------------------
/mcp-server/src/core/task-master-core.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * task-master-core.js
3 | * Central module that imports and re-exports all direct function implementations
4 | * for improved organization and maintainability.
5 | */
6 |
7 | // Import direct function implementations
8 | import { getCacheStatsDirect } from './direct-functions/cache-stats.js';
9 | import { parsePRDDirect } from './direct-functions/parse-prd.js';
10 | import { updateTasksDirect } from './direct-functions/update-tasks.js';
11 | import { updateTaskByIdDirect } from './direct-functions/update-task-by-id.js';
12 | import { updateSubtaskByIdDirect } from './direct-functions/update-subtask-by-id.js';
13 | import { generateTaskFilesDirect } from './direct-functions/generate-task-files.js';
14 | import { setTaskStatusDirect } from './direct-functions/set-task-status.js';
15 | import { nextTaskDirect } from './direct-functions/next-task.js';
16 | import { expandTaskDirect } from './direct-functions/expand-task.js';
17 | import { addTaskDirect } from './direct-functions/add-task.js';
18 | import { addSubtaskDirect } from './direct-functions/add-subtask.js';
19 | import { removeSubtaskDirect } from './direct-functions/remove-subtask.js';
20 | import { analyzeTaskComplexityDirect } from './direct-functions/analyze-task-complexity.js';
21 | import { clearSubtasksDirect } from './direct-functions/clear-subtasks.js';
22 | import { expandAllTasksDirect } from './direct-functions/expand-all-tasks.js';
23 | import { removeDependencyDirect } from './direct-functions/remove-dependency.js';
24 | import { validateDependenciesDirect } from './direct-functions/validate-dependencies.js';
25 | import { fixDependenciesDirect } from './direct-functions/fix-dependencies.js';
26 | import { complexityReportDirect } from './direct-functions/complexity-report.js';
27 | import { addDependencyDirect } from './direct-functions/add-dependency.js';
28 | import { removeTaskDirect } from './direct-functions/remove-task.js';
29 | import { initializeProjectDirect } from './direct-functions/initialize-project.js';
30 | import { modelsDirect } from './direct-functions/models.js';
31 | import { moveTaskDirect } from './direct-functions/move-task.js';
32 | import { moveTaskCrossTagDirect } from './direct-functions/move-task-cross-tag.js';
33 | import { researchDirect } from './direct-functions/research.js';
34 | import { addTagDirect } from './direct-functions/add-tag.js';
35 | import { deleteTagDirect } from './direct-functions/delete-tag.js';
36 | import { listTagsDirect } from './direct-functions/list-tags.js';
37 | import { useTagDirect } from './direct-functions/use-tag.js';
38 | import { renameTagDirect } from './direct-functions/rename-tag.js';
39 | import { copyTagDirect } from './direct-functions/copy-tag.js';
40 | import { scopeUpDirect } from './direct-functions/scope-up.js';
41 | import { scopeDownDirect } from './direct-functions/scope-down.js';
42 |
43 | // Re-export utility functions
44 | export { findTasksPath } from './utils/path-utils.js';
45 |
46 | // Use Map for potential future enhancements like introspection or dynamic dispatch
47 | export const directFunctions = new Map([
48 | ['getCacheStatsDirect', getCacheStatsDirect],
49 | ['parsePRDDirect', parsePRDDirect],
50 | ['updateTasksDirect', updateTasksDirect],
51 | ['updateTaskByIdDirect', updateTaskByIdDirect],
52 | ['updateSubtaskByIdDirect', updateSubtaskByIdDirect],
53 | ['generateTaskFilesDirect', generateTaskFilesDirect],
54 | ['setTaskStatusDirect', setTaskStatusDirect],
55 | ['nextTaskDirect', nextTaskDirect],
56 | ['expandTaskDirect', expandTaskDirect],
57 | ['addTaskDirect', addTaskDirect],
58 | ['addSubtaskDirect', addSubtaskDirect],
59 | ['removeSubtaskDirect', removeSubtaskDirect],
60 | ['analyzeTaskComplexityDirect', analyzeTaskComplexityDirect],
61 | ['clearSubtasksDirect', clearSubtasksDirect],
62 | ['expandAllTasksDirect', expandAllTasksDirect],
63 | ['removeDependencyDirect', removeDependencyDirect],
64 | ['validateDependenciesDirect', validateDependenciesDirect],
65 | ['fixDependenciesDirect', fixDependenciesDirect],
66 | ['complexityReportDirect', complexityReportDirect],
67 | ['addDependencyDirect', addDependencyDirect],
68 | ['removeTaskDirect', removeTaskDirect],
69 | ['initializeProjectDirect', initializeProjectDirect],
70 | ['modelsDirect', modelsDirect],
71 | ['moveTaskDirect', moveTaskDirect],
72 | ['moveTaskCrossTagDirect', moveTaskCrossTagDirect],
73 | ['researchDirect', researchDirect],
74 | ['addTagDirect', addTagDirect],
75 | ['deleteTagDirect', deleteTagDirect],
76 | ['listTagsDirect', listTagsDirect],
77 | ['useTagDirect', useTagDirect],
78 | ['renameTagDirect', renameTagDirect],
79 | ['copyTagDirect', copyTagDirect],
80 | ['scopeUpDirect', scopeUpDirect],
81 | ['scopeDownDirect', scopeDownDirect]
82 | ]);
83 |
84 | // Re-export all direct function implementations
85 | export {
86 | getCacheStatsDirect,
87 | parsePRDDirect,
88 | updateTasksDirect,
89 | updateTaskByIdDirect,
90 | updateSubtaskByIdDirect,
91 | generateTaskFilesDirect,
92 | setTaskStatusDirect,
93 | nextTaskDirect,
94 | expandTaskDirect,
95 | addTaskDirect,
96 | addSubtaskDirect,
97 | removeSubtaskDirect,
98 | analyzeTaskComplexityDirect,
99 | clearSubtasksDirect,
100 | expandAllTasksDirect,
101 | removeDependencyDirect,
102 | validateDependenciesDirect,
103 | fixDependenciesDirect,
104 | complexityReportDirect,
105 | addDependencyDirect,
106 | removeTaskDirect,
107 | initializeProjectDirect,
108 | modelsDirect,
109 | moveTaskDirect,
110 | moveTaskCrossTagDirect,
111 | researchDirect,
112 | addTagDirect,
113 | deleteTagDirect,
114 | listTagsDirect,
115 | useTagDirect,
116 | renameTagDirect,
117 | copyTagDirect,
118 | scopeUpDirect,
119 | scopeDownDirect
120 | };
121 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/git/git-domain.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Git Domain Facade
3 | * Public API for Git operations
4 | */
5 |
6 | import type { StatusResult } from 'simple-git';
7 | import { GitAdapter } from './adapters/git-adapter.js';
8 | import { CommitMessageGenerator } from './services/commit-message-generator.js';
9 | import type { CommitMessageOptions } from './services/commit-message-generator.js';
10 |
11 | /**
12 | * Git Domain - Unified API for Git operations
13 | */
14 | export class GitDomain {
15 | private gitAdapter: GitAdapter;
16 | private commitGenerator: CommitMessageGenerator;
17 |
18 | constructor(projectPath: string) {
19 | this.gitAdapter = new GitAdapter(projectPath);
20 | this.commitGenerator = new CommitMessageGenerator();
21 | }
22 |
23 | // ========== Repository Validation ==========
24 |
25 | /**
26 | * Check if directory is a git repository
27 | */
28 | async isGitRepository(): Promise<boolean> {
29 | return this.gitAdapter.isGitRepository();
30 | }
31 |
32 | /**
33 | * Ensure we're in a valid git repository
34 | */
35 | async ensureGitRepository(): Promise<void> {
36 | return this.gitAdapter.ensureGitRepository();
37 | }
38 |
39 | /**
40 | * Get repository root path
41 | */
42 | async getRepositoryRoot(): Promise<string> {
43 | return this.gitAdapter.getRepositoryRoot();
44 | }
45 |
46 | // ========== Working Tree Status ==========
47 |
48 | /**
49 | * Check if working tree is clean
50 | */
51 | async isWorkingTreeClean(): Promise<boolean> {
52 | return this.gitAdapter.isWorkingTreeClean();
53 | }
54 |
55 | /**
56 | * Get git status
57 | */
58 | async getStatus(): Promise<StatusResult> {
59 | return this.gitAdapter.getStatus();
60 | }
61 |
62 | /**
63 | * Get status summary
64 | */
65 | async getStatusSummary(): Promise<{
66 | isClean: boolean;
67 | staged: number;
68 | modified: number;
69 | deleted: number;
70 | untracked: number;
71 | totalChanges: number;
72 | }> {
73 | return this.gitAdapter.getStatusSummary();
74 | }
75 |
76 | /**
77 | * Check if there are uncommitted changes
78 | */
79 | async hasUncommittedChanges(): Promise<boolean> {
80 | return this.gitAdapter.hasUncommittedChanges();
81 | }
82 |
83 | /**
84 | * Check if there are staged changes
85 | */
86 | async hasStagedChanges(): Promise<boolean> {
87 | return this.gitAdapter.hasStagedChanges();
88 | }
89 |
90 | // ========== Branch Operations ==========
91 |
92 | /**
93 | * Get current branch name
94 | */
95 | async getCurrentBranch(): Promise<string> {
96 | return this.gitAdapter.getCurrentBranch();
97 | }
98 |
99 | /**
100 | * List all local branches
101 | */
102 | async listBranches(): Promise<string[]> {
103 | return this.gitAdapter.listBranches();
104 | }
105 |
106 | /**
107 | * Check if a branch exists
108 | */
109 | async branchExists(branchName: string): Promise<boolean> {
110 | return this.gitAdapter.branchExists(branchName);
111 | }
112 |
113 | /**
114 | * Create a new branch
115 | */
116 | async createBranch(
117 | branchName: string,
118 | options?: { checkout?: boolean }
119 | ): Promise<void> {
120 | return this.gitAdapter.createBranch(branchName, options);
121 | }
122 |
123 | /**
124 | * Checkout an existing branch
125 | */
126 | async checkoutBranch(
127 | branchName: string,
128 | options?: { force?: boolean }
129 | ): Promise<void> {
130 | return this.gitAdapter.checkoutBranch(branchName, options);
131 | }
132 |
133 | /**
134 | * Create and checkout a new branch
135 | */
136 | async createAndCheckoutBranch(branchName: string): Promise<void> {
137 | return this.gitAdapter.createAndCheckoutBranch(branchName);
138 | }
139 |
140 | /**
141 | * Delete a branch
142 | */
143 | async deleteBranch(
144 | branchName: string,
145 | options?: { force?: boolean }
146 | ): Promise<void> {
147 | return this.gitAdapter.deleteBranch(branchName, options);
148 | }
149 |
150 | /**
151 | * Get default branch name
152 | */
153 | async getDefaultBranch(): Promise<string> {
154 | return this.gitAdapter.getDefaultBranch();
155 | }
156 |
157 | /**
158 | * Check if on default branch
159 | */
160 | async isOnDefaultBranch(): Promise<boolean> {
161 | return this.gitAdapter.isOnDefaultBranch();
162 | }
163 |
164 | // ========== Commit Operations ==========
165 |
166 | /**
167 | * Stage files for commit
168 | */
169 | async stageFiles(files: string[]): Promise<void> {
170 | return this.gitAdapter.stageFiles(files);
171 | }
172 |
173 | /**
174 | * Unstage files
175 | */
176 | async unstageFiles(files: string[]): Promise<void> {
177 | return this.gitAdapter.unstageFiles(files);
178 | }
179 |
180 | /**
181 | * Create a commit
182 | */
183 | async createCommit(
184 | message: string,
185 | options?: {
186 | metadata?: Record<string, string>;
187 | allowEmpty?: boolean;
188 | enforceNonDefaultBranch?: boolean;
189 | force?: boolean;
190 | }
191 | ): Promise<void> {
192 | return this.gitAdapter.createCommit(message, options);
193 | }
194 |
195 | /**
196 | * Get commit log
197 | */
198 | async getCommitLog(options?: { maxCount?: number }): Promise<any[]> {
199 | return this.gitAdapter.getCommitLog(options);
200 | }
201 |
202 | /**
203 | * Get last commit
204 | */
205 | async getLastCommit(): Promise<any> {
206 | return this.gitAdapter.getLastCommit();
207 | }
208 |
209 | // ========== Remote Operations ==========
210 |
211 | /**
212 | * Check if repository has remotes
213 | */
214 | async hasRemote(): Promise<boolean> {
215 | return this.gitAdapter.hasRemote();
216 | }
217 |
218 | /**
219 | * Get all configured remotes
220 | */
221 | async getRemotes(): Promise<any[]> {
222 | return this.gitAdapter.getRemotes();
223 | }
224 |
225 | // ========== Commit Message Generation ==========
226 |
227 | /**
228 | * Generate a conventional commit message
229 | */
230 | generateCommitMessage(options: CommitMessageOptions): string {
231 | return this.commitGenerator.generateMessage(options);
232 | }
233 |
234 | /**
235 | * Validate a conventional commit message
236 | */
237 | validateCommitMessage(message: string) {
238 | return this.commitGenerator.validateConventionalCommit(message);
239 | }
240 |
241 | /**
242 | * Parse a commit message
243 | */
244 | parseCommitMessage(message: string) {
245 | return this.commitGenerator.parseCommitMessage(message);
246 | }
247 | }
248 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/kilo-integration.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 | import fs from 'fs';
3 | import path from 'path';
4 | import os from 'os';
5 |
6 | // Mock external modules
7 | jest.mock('child_process', () => ({
8 | execSync: jest.fn()
9 | }));
10 |
11 | // Mock console methods
12 | jest.mock('console', () => ({
13 | log: jest.fn(),
14 | info: jest.fn(),
15 | warn: jest.fn(),
16 | error: jest.fn(),
17 | clear: jest.fn()
18 | }));
19 |
20 | describe('Kilo Integration', () => {
21 | let tempDir;
22 |
23 | beforeEach(() => {
24 | jest.clearAllMocks();
25 |
26 | // Create a temporary directory for testing
27 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-'));
28 |
29 | // Spy on fs methods
30 | jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {});
31 | jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => {
32 | if (filePath.toString().includes('.kilocodemodes')) {
33 | return 'Existing kilocodemodes content';
34 | }
35 | if (filePath.toString().includes('-rules')) {
36 | return 'Existing mode rules content';
37 | }
38 | return '{}';
39 | });
40 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
41 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
42 | });
43 |
44 | afterEach(() => {
45 | // Clean up the temporary directory
46 | try {
47 | fs.rmSync(tempDir, { recursive: true, force: true });
48 | } catch (err) {
49 | console.error(`Error cleaning up: ${err.message}`);
50 | }
51 | });
52 |
53 | // Test function that simulates the createProjectStructure behavior for Kilo files
54 | function mockCreateKiloStructure() {
55 | // Create main .kilo directory
56 | fs.mkdirSync(path.join(tempDir, '.kilo'), { recursive: true });
57 |
58 | // Create rules directory
59 | fs.mkdirSync(path.join(tempDir, '.kilo', 'rules'), { recursive: true });
60 |
61 | // Create mode-specific rule directories
62 | const kiloModes = [
63 | 'architect',
64 | 'ask',
65 | 'orchestrator',
66 | 'code',
67 | 'debug',
68 | 'test'
69 | ];
70 | for (const mode of kiloModes) {
71 | fs.mkdirSync(path.join(tempDir, '.kilo', `rules-${mode}`), {
72 | recursive: true
73 | });
74 | fs.writeFileSync(
75 | path.join(tempDir, '.kilo', `rules-${mode}`, `${mode}-rules`),
76 | `Content for ${mode} rules`
77 | );
78 | }
79 |
80 | // Create additional directories
81 | fs.mkdirSync(path.join(tempDir, '.kilo', 'config'), { recursive: true });
82 | fs.mkdirSync(path.join(tempDir, '.kilo', 'templates'), { recursive: true });
83 | fs.mkdirSync(path.join(tempDir, '.kilo', 'logs'), { recursive: true });
84 |
85 | // Copy .kilocodemodes file
86 | fs.writeFileSync(
87 | path.join(tempDir, '.kilocodemodes'),
88 | 'Kilocodemodes file content'
89 | );
90 | }
91 |
92 | test('creates all required .kilo directories', () => {
93 | // Act
94 | mockCreateKiloStructure();
95 |
96 | // Assert
97 | expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.kilo'), {
98 | recursive: true
99 | });
100 | expect(fs.mkdirSync).toHaveBeenCalledWith(
101 | path.join(tempDir, '.kilo', 'rules'),
102 | { recursive: true }
103 | );
104 |
105 | // Verify all mode directories are created
106 | expect(fs.mkdirSync).toHaveBeenCalledWith(
107 | path.join(tempDir, '.kilo', 'rules-architect'),
108 | { recursive: true }
109 | );
110 | expect(fs.mkdirSync).toHaveBeenCalledWith(
111 | path.join(tempDir, '.kilo', 'rules-ask'),
112 | { recursive: true }
113 | );
114 | expect(fs.mkdirSync).toHaveBeenCalledWith(
115 | path.join(tempDir, '.kilo', 'rules-orchestrator'),
116 | { recursive: true }
117 | );
118 | expect(fs.mkdirSync).toHaveBeenCalledWith(
119 | path.join(tempDir, '.kilo', 'rules-code'),
120 | { recursive: true }
121 | );
122 | expect(fs.mkdirSync).toHaveBeenCalledWith(
123 | path.join(tempDir, '.kilo', 'rules-debug'),
124 | { recursive: true }
125 | );
126 | expect(fs.mkdirSync).toHaveBeenCalledWith(
127 | path.join(tempDir, '.kilo', 'rules-test'),
128 | { recursive: true }
129 | );
130 | });
131 |
132 | test('creates rule files for all modes', () => {
133 | // Act
134 | mockCreateKiloStructure();
135 |
136 | // Assert - check all rule files are created
137 | expect(fs.writeFileSync).toHaveBeenCalledWith(
138 | path.join(tempDir, '.kilo', 'rules-architect', 'architect-rules'),
139 | expect.any(String)
140 | );
141 | expect(fs.writeFileSync).toHaveBeenCalledWith(
142 | path.join(tempDir, '.kilo', 'rules-ask', 'ask-rules'),
143 | expect.any(String)
144 | );
145 | expect(fs.writeFileSync).toHaveBeenCalledWith(
146 | path.join(tempDir, '.kilo', 'rules-orchestrator', 'orchestrator-rules'),
147 | expect.any(String)
148 | );
149 | expect(fs.writeFileSync).toHaveBeenCalledWith(
150 | path.join(tempDir, '.kilo', 'rules-code', 'code-rules'),
151 | expect.any(String)
152 | );
153 | expect(fs.writeFileSync).toHaveBeenCalledWith(
154 | path.join(tempDir, '.kilo', 'rules-debug', 'debug-rules'),
155 | expect.any(String)
156 | );
157 | expect(fs.writeFileSync).toHaveBeenCalledWith(
158 | path.join(tempDir, '.kilo', 'rules-test', 'test-rules'),
159 | expect.any(String)
160 | );
161 | });
162 |
163 | test('creates .kilocodemodes file in project root', () => {
164 | // Act
165 | mockCreateKiloStructure();
166 |
167 | // Assert
168 | expect(fs.writeFileSync).toHaveBeenCalledWith(
169 | path.join(tempDir, '.kilocodemodes'),
170 | expect.any(String)
171 | );
172 | });
173 |
174 | test('creates additional required Kilo directories', () => {
175 | // Act
176 | mockCreateKiloStructure();
177 |
178 | // Assert
179 | expect(fs.mkdirSync).toHaveBeenCalledWith(
180 | path.join(tempDir, '.kilo', 'config'),
181 | { recursive: true }
182 | );
183 | expect(fs.mkdirSync).toHaveBeenCalledWith(
184 | path.join(tempDir, '.kilo', 'templates'),
185 | { recursive: true }
186 | );
187 | expect(fs.mkdirSync).toHaveBeenCalledWith(
188 | path.join(tempDir, '.kilo', 'logs'),
189 | { recursive: true }
190 | );
191 | });
192 | });
193 |
```
--------------------------------------------------------------------------------
/apps/cli/tests/unit/commands/list.command.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Unit tests for ListTasksCommand
3 | */
4 |
5 | import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
6 | import type { TmCore } from '@tm/core';
7 |
8 | // Mock dependencies
9 | vi.mock('@tm/core', () => ({
10 | createTmCore: vi.fn(),
11 | OUTPUT_FORMATS: ['text', 'json', 'compact'],
12 | TASK_STATUSES: [
13 | 'pending',
14 | 'in-progress',
15 | 'done',
16 | 'review',
17 | 'deferred',
18 | 'cancelled'
19 | ],
20 | STATUS_ICONS: {
21 | pending: '⏳',
22 | 'in-progress': '🔄',
23 | done: '✅',
24 | review: '👀',
25 | deferred: '⏸️',
26 | cancelled: '❌'
27 | }
28 | }));
29 |
30 | vi.mock('../../../src/utils/project-root.js', () => ({
31 | getProjectRoot: vi.fn((path?: string) => path || '/test/project')
32 | }));
33 |
34 | vi.mock('../../../src/utils/error-handler.js', () => ({
35 | displayError: vi.fn()
36 | }));
37 |
38 | vi.mock('../../../src/utils/display-helpers.js', () => ({
39 | displayCommandHeader: vi.fn()
40 | }));
41 |
42 | vi.mock('../../../src/ui/index.js', () => ({
43 | calculateDependencyStatistics: vi.fn(() => ({ total: 0, blocked: 0 })),
44 | calculateSubtaskStatistics: vi.fn(() => ({ total: 0, completed: 0 })),
45 | calculateTaskStatistics: vi.fn(() => ({ total: 0, completed: 0 })),
46 | displayDashboards: vi.fn(),
47 | displayRecommendedNextTask: vi.fn(),
48 | displaySuggestedNextSteps: vi.fn(),
49 | getPriorityBreakdown: vi.fn(() => ({})),
50 | getTaskDescription: vi.fn(() => 'Test description')
51 | }));
52 |
53 | vi.mock('../../../src/utils/ui.js', () => ({
54 | createTaskTable: vi.fn(() => 'Table output'),
55 | displayWarning: vi.fn()
56 | }));
57 |
58 | import { ListTasksCommand } from '../../../src/commands/list.command.js';
59 |
60 | describe('ListTasksCommand', () => {
61 | let consoleLogSpy: any;
62 | let mockTmCore: Partial<TmCore>;
63 |
64 | beforeEach(() => {
65 | consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
66 |
67 | mockTmCore = {
68 | tasks: {
69 | list: vi.fn().mockResolvedValue({
70 | tasks: [{ id: '1', title: 'Test Task', status: 'pending' }],
71 | total: 1,
72 | filtered: 1,
73 | storageType: 'json'
74 | }),
75 | getStorageType: vi.fn().mockReturnValue('json')
76 | } as any,
77 | config: {
78 | getActiveTag: vi.fn().mockReturnValue('master')
79 | } as any
80 | };
81 | });
82 |
83 | afterEach(() => {
84 | vi.clearAllMocks();
85 | consoleLogSpy.mockRestore();
86 | });
87 |
88 | describe('JSON output format', () => {
89 | it('should use JSON format when --json flag is set', async () => {
90 | const command = new ListTasksCommand();
91 |
92 | // Mock the tmCore initialization
93 | (command as any).tmCore = mockTmCore;
94 |
95 | // Execute with --json flag
96 | await (command as any).executeCommand({
97 | json: true,
98 | format: 'text' // Should be overridden by --json
99 | });
100 |
101 | // Verify JSON output was called
102 | expect(consoleLogSpy).toHaveBeenCalled();
103 | const output = consoleLogSpy.mock.calls[0][0];
104 |
105 | // Should be valid JSON
106 | expect(() => JSON.parse(output)).not.toThrow();
107 |
108 | const parsed = JSON.parse(output);
109 | expect(parsed).toHaveProperty('tasks');
110 | expect(parsed).toHaveProperty('metadata');
111 | });
112 |
113 | it('should override --format when --json is set', async () => {
114 | const command = new ListTasksCommand();
115 | (command as any).tmCore = mockTmCore;
116 |
117 | await (command as any).executeCommand({
118 | json: true,
119 | format: 'compact' // Should be overridden
120 | });
121 |
122 | // Should output JSON, not compact format
123 | const output = consoleLogSpy.mock.calls[0][0];
124 | expect(() => JSON.parse(output)).not.toThrow();
125 | });
126 |
127 | it('should use specified format when --json is not set', async () => {
128 | const command = new ListTasksCommand();
129 | (command as any).tmCore = mockTmCore;
130 |
131 | await (command as any).executeCommand({
132 | format: 'compact'
133 | });
134 |
135 | // Should use compact format (not JSON)
136 | const output = consoleLogSpy.mock.calls;
137 | // In compact mode, output is not JSON
138 | expect(output.length).toBeGreaterThan(0);
139 | });
140 |
141 | it('should default to text format when neither flag is set', async () => {
142 | const command = new ListTasksCommand();
143 | (command as any).tmCore = mockTmCore;
144 |
145 | await (command as any).executeCommand({});
146 |
147 | // Should use text format (not JSON)
148 | // If any console.log was called, verify it's not JSON
149 | if (consoleLogSpy.mock.calls.length > 0) {
150 | const output = consoleLogSpy.mock.calls[0][0];
151 | // Text format output should not be parseable JSON
152 | // or should be the table string we mocked
153 | expect(
154 | output === 'Table output' ||
155 | (() => {
156 | try {
157 | JSON.parse(output);
158 | return false;
159 | } catch {
160 | return true;
161 | }
162 | })()
163 | ).toBe(true);
164 | }
165 | });
166 | });
167 |
168 | describe('format validation', () => {
169 | it('should accept valid formats', () => {
170 | const command = new ListTasksCommand();
171 |
172 | expect((command as any).validateOptions({ format: 'text' })).toBe(true);
173 | expect((command as any).validateOptions({ format: 'json' })).toBe(true);
174 | expect((command as any).validateOptions({ format: 'compact' })).toBe(
175 | true
176 | );
177 | });
178 |
179 | it('should reject invalid formats', () => {
180 | const consoleErrorSpy = vi
181 | .spyOn(console, 'error')
182 | .mockImplementation(() => {});
183 | const command = new ListTasksCommand();
184 |
185 | expect((command as any).validateOptions({ format: 'invalid' })).toBe(
186 | false
187 | );
188 | expect(consoleErrorSpy).toHaveBeenCalledWith(
189 | expect.stringContaining('Invalid format: invalid')
190 | );
191 |
192 | consoleErrorSpy.mockRestore();
193 | });
194 | });
195 | });
196 |
```
--------------------------------------------------------------------------------
/apps/extension/package.mjs:
--------------------------------------------------------------------------------
```
1 | import { execSync } from 'child_process';
2 | import path from 'path';
3 | import { fileURLToPath } from 'url';
4 | import fs from 'fs-extra';
5 |
6 | // --- Configuration ---
7 | const __filename = fileURLToPath(import.meta.url);
8 | const __dirname = path.dirname(__filename);
9 |
10 | const packageDir = path.resolve(__dirname, 'vsix-build');
11 | // --- End Configuration ---
12 |
13 | try {
14 | console.log('🚀 Starting packaging process...');
15 |
16 | // 1. Build Project
17 | console.log('\nBuilding JavaScript...');
18 | execSync('npm run build:js', { stdio: 'inherit' });
19 | console.log('\nBuilding CSS...');
20 | execSync('npm run build:css', { stdio: 'inherit' });
21 |
22 | // 2. Prepare Clean Directory
23 | console.log(`\nPreparing clean directory at: ${packageDir}`);
24 | fs.emptyDirSync(packageDir);
25 |
26 | // 3. Copy Build Artifacts (excluding source maps)
27 | console.log('Copying build artifacts...');
28 | const distDir = path.resolve(__dirname, 'dist');
29 | const targetDistDir = path.resolve(packageDir, 'dist');
30 | fs.ensureDirSync(targetDistDir);
31 |
32 | // Only copy the files we need (exclude .map files)
33 | const filesToCopy = ['extension.js', 'index.js', 'index.css', 'sidebar.js'];
34 | for (const file of filesToCopy) {
35 | const srcFile = path.resolve(distDir, file);
36 | const destFile = path.resolve(targetDistDir, file);
37 | if (fs.existsSync(srcFile)) {
38 | fs.copySync(srcFile, destFile);
39 | console.log(` - Copied dist/${file}`);
40 | }
41 | }
42 |
43 | // 4. Copy additional files
44 | const additionalFiles = ['README.md', 'CHANGELOG.md', 'AGENTS.md'];
45 | for (const file of additionalFiles) {
46 | if (fs.existsSync(path.resolve(__dirname, file))) {
47 | fs.copySync(
48 | path.resolve(__dirname, file),
49 | path.resolve(packageDir, file)
50 | );
51 | console.log(` - Copied ${file}`);
52 | }
53 | }
54 |
55 | // 5. Sync versions and prepare the final package.json
56 | console.log('Syncing versions and preparing the final package.json...');
57 |
58 | // Read current versions
59 | const devPackagePath = path.resolve(__dirname, 'package.json');
60 | const publishPackagePath = path.resolve(__dirname, 'package.publish.json');
61 |
62 | const devPackage = JSON.parse(fs.readFileSync(devPackagePath, 'utf8'));
63 | const publishPackage = JSON.parse(
64 | fs.readFileSync(publishPackagePath, 'utf8')
65 | );
66 |
67 | // Handle RC versions for VS Code Marketplace
68 | let finalVersion = devPackage.version;
69 | if (finalVersion.includes('-rc.')) {
70 | console.log(
71 | ' - Detected RC version, transforming for VS Code Marketplace...'
72 | );
73 |
74 | // Extract base version and RC number
75 | const baseVersion = finalVersion.replace(/-rc\.\d+$/, '');
76 | const rcMatch = finalVersion.match(/rc\.(\d+)/);
77 | const rcNumber = rcMatch ? parseInt(rcMatch[1]) : 0;
78 |
79 | // For each RC iteration, increment the patch version
80 | // This ensures unique versions in VS Code Marketplace
81 | if (rcNumber > 0) {
82 | const [major, minor, patch] = baseVersion.split('.').map(Number);
83 | finalVersion = `${major}.${minor}.${patch + rcNumber}`;
84 | console.log(
85 | ` - RC version mapping: ${devPackage.version} → ${finalVersion}`
86 | );
87 | } else {
88 | finalVersion = baseVersion;
89 | console.log(
90 | ` - RC version mapping: ${devPackage.version} → ${finalVersion}`
91 | );
92 | }
93 | }
94 |
95 | // Check if versions need updating
96 | if (publishPackage.version !== finalVersion) {
97 | console.log(
98 | ` - Version sync needed: ${publishPackage.version} → ${finalVersion}`
99 | );
100 | publishPackage.version = finalVersion;
101 |
102 | // Update the source package.publish.json file with the final version
103 | fs.writeFileSync(
104 | publishPackagePath,
105 | JSON.stringify(publishPackage, null, '\t') + '\n'
106 | );
107 | console.log(` - Updated package.publish.json version to ${finalVersion}`);
108 | } else {
109 | console.log(` - Versions already in sync: ${finalVersion}`);
110 | }
111 |
112 | // Copy the (now synced) package.publish.json as package.json
113 | fs.copySync(publishPackagePath, path.resolve(packageDir, 'package.json'));
114 | console.log(' - Copied package.publish.json as package.json');
115 |
116 | // 6. Copy .vscodeignore if it exists
117 | if (fs.existsSync(path.resolve(__dirname, '.vscodeignore'))) {
118 | fs.copySync(
119 | path.resolve(__dirname, '.vscodeignore'),
120 | path.resolve(packageDir, '.vscodeignore')
121 | );
122 | console.log(' - Copied .vscodeignore');
123 | }
124 |
125 | // 7. Copy LICENSE if it exists
126 | if (fs.existsSync(path.resolve(__dirname, 'LICENSE'))) {
127 | fs.copySync(
128 | path.resolve(__dirname, 'LICENSE'),
129 | path.resolve(packageDir, 'LICENSE')
130 | );
131 | console.log(' - Copied LICENSE');
132 | }
133 |
134 | // 7a. Copy assets directory if it exists
135 | const assetsDir = path.resolve(__dirname, 'assets');
136 | if (fs.existsSync(assetsDir)) {
137 | const targetAssetsDir = path.resolve(packageDir, 'assets');
138 | fs.copySync(assetsDir, targetAssetsDir);
139 | console.log(' - Copied assets directory');
140 | }
141 |
142 | // Small delay to ensure file system operations complete
143 | await new Promise((resolve) => setTimeout(resolve, 100));
144 |
145 | // 8. Final step - manual packaging
146 | console.log('\n✅ Build preparation complete!');
147 | console.log('\nTo create the VSIX package, run:');
148 | console.log(
149 | '\x1b[36m%s\x1b[0m',
150 | `cd vsix-build && npx vsce package --no-dependencies`
151 | );
152 |
153 | // Use the transformed version for output
154 | console.log(
155 | `\nYour extension will be packaged to: vsix-build/task-master-${finalVersion}.vsix`
156 | );
157 | } catch (error) {
158 | console.error('\n❌ Packaging failed!');
159 | console.error(error.message);
160 | process.exit(1);
161 | }
162 |
```