This is page 14 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/apps/docs/getting-started/quick-start/configuration-quick.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Configuration
3 | sidebarTitle: "Configuration"
4 |
5 | ---
6 |
7 | Before getting started with Task Master, you'll need to set up your API keys. There are a couple of ways to do this depending on whether you're using the CLI or working inside MCP. It's also a good time to start getting familiar with the other configuration options available — even if you don’t need to adjust them yet, knowing what’s possible will help down the line.
8 |
9 | ## API Key Setup
10 |
11 | Task Master uses environment variables to securely store provider API keys and optional endpoint URLs.
12 |
13 | ### MCP Usage: mcp.json file
14 |
15 | For MCP/Cursor usage: Configure keys in the env section of your .cursor/mcp.json file.
16 |
17 | ```java .env lines icon="java"
18 | {
19 | "mcpServers": {
20 | "task-master-ai": {
21 | "command": "npx",
22 | "args": ["-y", "task-master-ai"],
23 | "env": {
24 | "ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE",
25 | "PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE",
26 | "OPENAI_API_KEY": "OPENAI_API_KEY_HERE",
27 | "GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE",
28 | "XAI_API_KEY": "XAI_API_KEY_HERE",
29 | "OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE",
30 | "MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE",
31 | "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE",
32 | "OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE",
33 | "GITHUB_API_KEY": "GITHUB_API_KEY_HERE"
34 | }
35 | }
36 | }
37 | }
38 | ```
39 |
40 | <Tip>
41 | **Optimize Context Usage**: You can control which Task Master MCP tools are loaded using the `TASK_MASTER_TOOLS` environment variable. This helps reduce LLM context usage by only loading the tools you need.
42 |
43 | Options:
44 | - `all` (default) - All 36 tools
45 | - `standard` - 15 commonly used tools
46 | - `core` or `lean` - 7 essential tools
47 |
48 | Example:
49 | ```json
50 | "env": {
51 | "TASK_MASTER_TOOLS": "standard",
52 | "ANTHROPIC_API_KEY": "your_key_here"
53 | }
54 | ```
55 |
56 | See the [MCP Tools documentation](/capabilities/mcp#configurable-tool-loading) for details.
57 | </Tip>
58 |
59 | ### CLI Usage: `.env` File
60 |
61 | Create a `.env` file in your project root and include the keys for the providers you plan to use:
62 |
63 |
64 |
65 | ```java .env lines icon="java"
66 | # Required API keys for providers configured in .taskmaster/config.json
67 | ANTHROPIC_API_KEY=sk-ant-api03-your-key-here
68 | PERPLEXITY_API_KEY=pplx-your-key-here
69 | # OPENAI_API_KEY=sk-your-key-here
70 | # GOOGLE_API_KEY=AIzaSy...
71 | # AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here
72 | # etc.
73 |
74 | # Optional Endpoint Overrides
75 | # Use a specific provider's base URL, e.g., for an OpenAI-compatible API
76 | # OPENAI_BASE_URL=https://api.third-party.com/v1
77 | #
78 | # Azure OpenAI Configuration
79 | # AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/ or https://your-endpoint-name.cognitiveservices.azure.com/openai/deployments
80 | # OLLAMA_BASE_URL=http://custom-ollama-host:11434/api
81 |
82 | # Google Vertex AI Configuration (Required if using 'vertex' provider)
83 | # VERTEX_PROJECT_ID=your-gcp-project-id
84 | ```
85 |
86 | ## What Else Can Be Configured?
87 |
88 | The main configuration file (`.taskmaster/config.json`) allows you to control nearly every aspect of Task Master’s behavior. Here’s a high-level look at what you can customize:
89 |
90 | <Tip>
91 | You don’t need to configure everything up front. Most settings can be left as defaults or updated later as your workflow evolves.
92 | </Tip>
93 |
94 | <Accordion title="View Configuration Options">
95 |
96 | ### Models and Providers
97 | - Role-based model setup: `main`, `research`, `fallback`
98 | - Provider selection (Anthropic, OpenAI, Perplexity, etc.)
99 | - Model IDs per role
100 | - Temperature, max tokens, and other generation settings
101 | - Custom base URLs for OpenAI-compatible APIs
102 |
103 | ### Global Settings
104 | - `logLevel`: Logging verbosity
105 | - `debug`: Enable/disable debug mode
106 | - `projectName`: Optional name for your project
107 | - `defaultTag`: Default tag for task grouping
108 | - `defaultSubtasks`: Number of subtasks to auto-generate
109 | - `defaultPriority`: Priority level for new tasks
110 |
111 | ### API Endpoint Overrides
112 | - `ollamaBaseURL`: Custom Ollama server URL
113 | - `azureBaseURL`: Global Azure endpoint
114 | - `vertexProjectId`: Google Vertex AI project ID
115 | - `vertexLocation`: Region for Vertex AI models
116 |
117 | ### Tag and Git Integration
118 | - Default tag context per project
119 | - Support for task isolation by tag
120 | - Manual tag creation from Git branches
121 |
122 | ### State Management
123 | - Active tag tracking
124 | - Migration state
125 | - Last tag switch timestamp
126 |
127 | </Accordion>
128 |
129 | <Note>
130 | For advanced configuration options and detailed customization, see our [Advanced Configuration Guide](/best-practices/configuration-advanced) page.
131 | </Note>
```
--------------------------------------------------------------------------------
/packages/tm-bridge/src/expand-bridge.ts:
--------------------------------------------------------------------------------
```typescript
1 | import boxen from 'boxen';
2 | import chalk from 'chalk';
3 | import ora from 'ora';
4 | import type { BaseBridgeParams } from './bridge-types.js';
5 | import { checkStorageType } from './bridge-utils.js';
6 |
7 | /**
8 | * Parameters for the expand bridge function
9 | */
10 | export interface ExpandBridgeParams extends BaseBridgeParams {
11 | /** Task ID (can be numeric "1" or alphanumeric "TAS-49") */
12 | taskId: string | number;
13 | /** Number of subtasks to generate (optional) */
14 | numSubtasks?: number;
15 | /** Whether to use research AI */
16 | useResearch?: boolean;
17 | /** Additional context for generation */
18 | additionalContext?: string;
19 | /** Force regeneration even if subtasks exist */
20 | force?: boolean;
21 | }
22 |
23 | /**
24 | * Result returned when API storage handles the expansion
25 | */
26 | export interface RemoteExpandResult {
27 | success: boolean;
28 | taskId: string | number;
29 | message: string;
30 | telemetryData: null;
31 | tagInfo: null;
32 | }
33 |
34 | /**
35 | * Shared bridge function for expand-task command.
36 | * Checks if using API storage and delegates to remote AI service if so.
37 | *
38 | * @param params - Bridge parameters
39 | * @returns Result object if API storage handled it, null if should fall through to file storage
40 | */
41 | export async function tryExpandViaRemote(
42 | params: ExpandBridgeParams
43 | ): Promise<RemoteExpandResult | null> {
44 | const {
45 | taskId,
46 | numSubtasks,
47 | useResearch = false,
48 | additionalContext,
49 | force = false,
50 | projectRoot,
51 | tag,
52 | isMCP = false,
53 | outputFormat = 'text',
54 | report
55 | } = params;
56 |
57 | // Check storage type using shared utility
58 | const { isApiStorage, tmCore } = await checkStorageType(
59 | projectRoot,
60 | report,
61 | 'falling back to file-based expansion'
62 | );
63 |
64 | if (!isApiStorage || !tmCore) {
65 | // Not API storage - signal caller to fall through to file-based logic
66 | return null;
67 | }
68 |
69 | // API STORAGE PATH: Delegate to remote AI service
70 | report('info', `Delegating expansion to Hamster for task ${taskId}`);
71 |
72 | // Show CLI output if not MCP
73 | if (!isMCP && outputFormat === 'text') {
74 | const showDebug = process.env.TM_DEBUG === '1';
75 | const contextPreview =
76 | showDebug && additionalContext
77 | ? `${additionalContext.substring(0, 60)}${additionalContext.length > 60 ? '...' : ''}`
78 | : additionalContext
79 | ? '[provided]'
80 | : '[none]';
81 |
82 | console.log(
83 | boxen(
84 | chalk.blue.bold(`Expanding Task via Hamster`) +
85 | '\n\n' +
86 | chalk.white(`Task ID: ${taskId}`) +
87 | '\n' +
88 | chalk.white(`Subtasks: ${numSubtasks || 'auto'}`) +
89 | '\n' +
90 | chalk.white(`Use Research: ${useResearch ? 'yes' : 'no'}`) +
91 | '\n' +
92 | chalk.white(`Force: ${force ? 'yes' : 'no'}`) +
93 | '\n' +
94 | chalk.white(`Context: ${contextPreview}`),
95 | {
96 | padding: 1,
97 | borderColor: 'blue',
98 | borderStyle: 'round',
99 | margin: { top: 1, bottom: 1 }
100 | }
101 | )
102 | );
103 | }
104 |
105 | const spinner =
106 | !isMCP && outputFormat === 'text'
107 | ? ora({ text: 'Expanding task on Hamster...', color: 'cyan' }).start()
108 | : null;
109 |
110 | try {
111 | // Call the API storage method which handles the remote expansion
112 | const result = await tmCore.tasks.expand(String(taskId), tag, {
113 | numSubtasks,
114 | useResearch,
115 | additionalContext,
116 | force
117 | });
118 |
119 | if (spinner) {
120 | spinner.succeed('Task expansion queued successfully');
121 | }
122 |
123 | if (outputFormat === 'text') {
124 | // Build message conditionally based on result
125 | let messageLines = [
126 | chalk.green(`Successfully queued expansion for task ${taskId}`),
127 | '',
128 | chalk.white('The task expansion has been queued on Hamster'),
129 | chalk.white('Subtasks will be generated in the background.')
130 | ];
131 |
132 | // Add task link if available
133 | if (result?.taskLink) {
134 | messageLines.push('');
135 | messageLines.push(
136 | chalk.white('View task: ') + chalk.blue.underline(result.taskLink)
137 | );
138 | }
139 |
140 | // Always add CLI alternative
141 | messageLines.push('');
142 | messageLines.push(
143 | chalk.dim(`Or run: ${chalk.yellow(`task-master show ${taskId}`)}`)
144 | );
145 |
146 | console.log(
147 | boxen(messageLines.join('\n'), {
148 | padding: 1,
149 | borderColor: 'green',
150 | borderStyle: 'round'
151 | })
152 | );
153 | }
154 |
155 | // Return success result - signals that we handled it
156 | return {
157 | success: true,
158 | taskId: taskId,
159 | message: result?.message || 'Task expansion queued via remote AI service',
160 | telemetryData: null,
161 | tagInfo: null
162 | };
163 | } catch (expandError) {
164 | if (spinner) {
165 | spinner.fail('Expansion failed');
166 | }
167 |
168 | // tm-core already formatted the error properly, just re-throw
169 | throw expandError;
170 | }
171 | }
172 |
```
--------------------------------------------------------------------------------
/apps/mcp/src/tools/autopilot/complete.tool.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview autopilot-complete MCP tool
3 | * Complete the current TDD phase with test result validation
4 | */
5 |
6 | import { z } from 'zod';
7 | import {
8 | handleApiResult,
9 | withNormalizedProjectRoot
10 | } from '../../shared/utils.js';
11 | import type { MCPContext } from '../../shared/types.js';
12 | import { WorkflowService } from '@tm/core';
13 | import type { FastMCP } from 'fastmcp';
14 |
15 | const CompletePhaseSchema = z.object({
16 | projectRoot: z
17 | .string()
18 | .describe('Absolute path to the project root directory'),
19 | testResults: z
20 | .object({
21 | total: z.number().describe('Total number of tests'),
22 | passed: z.number().describe('Number of passing tests'),
23 | failed: z.number().describe('Number of failing tests'),
24 | skipped: z.number().optional().describe('Number of skipped tests')
25 | })
26 | .describe('Test results from running the test suite')
27 | });
28 |
29 | type CompletePhaseArgs = z.infer<typeof CompletePhaseSchema>;
30 |
31 | /**
32 | * Register the autopilot_complete_phase tool with the MCP server
33 | */
34 | export function registerAutopilotCompleteTool(server: FastMCP) {
35 | server.addTool({
36 | name: 'autopilot_complete_phase',
37 | description:
38 | 'Complete the current TDD phase (RED, GREEN, or COMMIT) with test result validation. RED phase: expects failures (if 0 failures, feature is already implemented and subtask auto-completes). GREEN phase: expects all tests passing.',
39 | parameters: CompletePhaseSchema,
40 | execute: withNormalizedProjectRoot(
41 | async (args: CompletePhaseArgs, context: MCPContext) => {
42 | const { projectRoot, testResults } = args;
43 |
44 | try {
45 | context.log.info(
46 | `Completing current phase in workflow for ${projectRoot}`
47 | );
48 |
49 | const workflowService = new WorkflowService(projectRoot);
50 |
51 | // Check if workflow exists
52 | if (!(await workflowService.hasWorkflow())) {
53 | return handleApiResult({
54 | result: {
55 | success: false,
56 | error: {
57 | message:
58 | 'No active workflow found. Start a workflow with autopilot_start'
59 | }
60 | },
61 | log: context.log,
62 | projectRoot
63 | });
64 | }
65 |
66 | // Resume workflow to get current state
67 | await workflowService.resumeWorkflow();
68 | const currentStatus = workflowService.getStatus();
69 |
70 | // Validate that we're in a TDD phase (RED or GREEN)
71 | if (!currentStatus.tddPhase) {
72 | return handleApiResult({
73 | result: {
74 | success: false,
75 | error: {
76 | message: `Cannot complete phase: not in a TDD phase (current phase: ${currentStatus.phase})`
77 | }
78 | },
79 | log: context.log,
80 | projectRoot
81 | });
82 | }
83 |
84 | // COMMIT phase completion is handled by autopilot_commit tool
85 | if (currentStatus.tddPhase === 'COMMIT') {
86 | return handleApiResult({
87 | result: {
88 | success: false,
89 | error: {
90 | message:
91 | 'Cannot complete COMMIT phase with this tool. Use autopilot_commit instead'
92 | }
93 | },
94 | log: context.log,
95 | projectRoot
96 | });
97 | }
98 |
99 | // Map TDD phase to TestResult phase (only RED or GREEN allowed)
100 | const phase = currentStatus.tddPhase as 'RED' | 'GREEN';
101 |
102 | // Construct full TestResult with phase
103 | const fullTestResults = {
104 | total: testResults.total,
105 | passed: testResults.passed,
106 | failed: testResults.failed,
107 | skipped: testResults.skipped ?? 0,
108 | phase
109 | };
110 |
111 | // Complete phase with test results
112 | const status = await workflowService.completePhase(fullTestResults);
113 | const nextAction = workflowService.getNextAction();
114 |
115 | context.log.info(
116 | `Phase completed. New phase: ${status.tddPhase || status.phase}`
117 | );
118 |
119 | return handleApiResult({
120 | result: {
121 | success: true,
122 | data: {
123 | message: `Phase completed. Transitioned to ${status.tddPhase || status.phase}`,
124 | ...status,
125 | nextAction: nextAction.action,
126 | actionDescription: nextAction.description,
127 | nextSteps: nextAction.nextSteps
128 | }
129 | },
130 | log: context.log,
131 | projectRoot
132 | });
133 | } catch (error: any) {
134 | context.log.error(`Error in autopilot-complete: ${error.message}`);
135 | if (error.stack) {
136 | context.log.debug(error.stack);
137 | }
138 | return handleApiResult({
139 | result: {
140 | success: false,
141 | error: {
142 | message: `Failed to complete phase: ${error.message}`
143 | }
144 | },
145 | log: context.log,
146 | projectRoot
147 | });
148 | }
149 | }
150 | )
151 | });
152 | }
153 |
```
--------------------------------------------------------------------------------
/apps/cli/src/commands/autopilot/commit.command.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Commit Command - Create commit with enhanced message generation
3 | */
4 |
5 | import { CommitMessageGenerator, GitAdapter, WorkflowService } from '@tm/core';
6 | import { Command } from 'commander';
7 | import { AutopilotBaseOptions, OutputFormatter } from './shared.js';
8 | import { getProjectRoot } from '../../utils/project-root.js';
9 |
10 | type CommitOptions = AutopilotBaseOptions;
11 |
12 | /**
13 | * Commit Command - Create commit using enhanced message generator
14 | */
15 | export class CommitCommand extends Command {
16 | constructor() {
17 | super('commit');
18 |
19 | this.description('Create a commit for the completed GREEN phase').action(
20 | async (options: CommitOptions) => {
21 | await this.execute(options);
22 | }
23 | );
24 | }
25 |
26 | private async execute(options: CommitOptions): Promise<void> {
27 | // Inherit parent options
28 | const parentOpts = this.parent?.opts() as AutopilotBaseOptions;
29 | const mergedOptions: CommitOptions = {
30 | ...parentOpts,
31 | ...options,
32 | projectRoot: getProjectRoot(
33 | options.projectRoot || parentOpts?.projectRoot
34 | )
35 | };
36 |
37 | const formatter = new OutputFormatter(mergedOptions.json || false);
38 |
39 | try {
40 | const projectRoot = mergedOptions.projectRoot!;
41 |
42 | // Create workflow service (manages WorkflowStateManager internally)
43 | const workflowService = new WorkflowService(projectRoot);
44 |
45 | // Check if workflow exists
46 | if (!(await workflowService.hasWorkflow())) {
47 | formatter.error('No active workflow', {
48 | suggestion: 'Start a workflow with: autopilot start <taskId>'
49 | });
50 | process.exit(1);
51 | }
52 |
53 | // Resume workflow (loads state with single WorkflowStateManager instance)
54 | await workflowService.resumeWorkflow();
55 | const status = workflowService.getStatus();
56 | const workflowContext = workflowService.getContext();
57 |
58 | // Verify in COMMIT phase
59 | if (status.tddPhase !== 'COMMIT') {
60 | formatter.error('Not in COMMIT phase', {
61 | currentPhase: status.tddPhase || status.phase,
62 | suggestion: 'Complete RED and GREEN phases first'
63 | });
64 | process.exit(1);
65 | }
66 |
67 | // Verify there's an active subtask
68 | if (!status.currentSubtask) {
69 | formatter.error('No current subtask');
70 | process.exit(1);
71 | }
72 |
73 | // Initialize git adapter
74 | const gitAdapter = new GitAdapter(projectRoot);
75 | await gitAdapter.ensureGitRepository();
76 |
77 | // Check for staged changes
78 | const hasStagedChanges = await gitAdapter.hasStagedChanges();
79 | if (!hasStagedChanges) {
80 | // Stage all changes
81 | formatter.info('No staged changes, staging all changes...');
82 | await gitAdapter.stageFiles(['.']);
83 | }
84 |
85 | // Get changed files for scope detection
86 | const gitStatus = await gitAdapter.getStatus();
87 | const changedFiles = [...gitStatus.staged, ...gitStatus.modified];
88 |
89 | // Generate commit message
90 | const messageGenerator = new CommitMessageGenerator();
91 | const testResults = workflowContext.lastTestResults;
92 |
93 | const commitMessage = messageGenerator.generateMessage({
94 | type: 'feat',
95 | description: status.currentSubtask.title,
96 | changedFiles,
97 | taskId: status.taskId,
98 | phase: status.tddPhase,
99 | tag: (workflowContext.metadata.tag as string) || undefined,
100 | testsPassing: testResults?.passed,
101 | testsFailing: testResults?.failed,
102 | coveragePercent: undefined // Could be added if available
103 | });
104 |
105 | // Create commit with metadata
106 | await gitAdapter.createCommit(commitMessage, {
107 | metadata: {
108 | taskId: status.taskId,
109 | subtaskId: status.currentSubtask.id,
110 | phase: 'COMMIT',
111 | tddCycle: 'complete'
112 | }
113 | });
114 |
115 | // Get commit info
116 | const lastCommit = await gitAdapter.getLastCommit();
117 |
118 | // Complete COMMIT phase and advance workflow
119 | // This handles all transitions internally with a single WorkflowStateManager
120 | const newStatus = await workflowService.commit();
121 |
122 | const isComplete = newStatus.phase === 'COMPLETE';
123 |
124 | // Output success
125 | formatter.success('Commit created', {
126 | commitHash: lastCommit.hash.substring(0, 7),
127 | message: commitMessage.split('\n')[0], // First line only
128 | subtask: {
129 | id: status.currentSubtask.id,
130 | title: status.currentSubtask.title
131 | },
132 | progress: newStatus.progress,
133 | nextAction: isComplete
134 | ? 'All subtasks complete. Run: autopilot status'
135 | : 'Start next subtask with RED phase'
136 | });
137 | } catch (error) {
138 | formatter.error((error as Error).message);
139 | if (mergedOptions.verbose) {
140 | console.error((error as Error).stack);
141 | }
142 | process.exit(1);
143 | }
144 | }
145 | }
146 |
```
--------------------------------------------------------------------------------
/mcp-server/src/core/context-manager.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * context-manager.js
3 | * Context and cache management for Task Master MCP Server
4 | */
5 |
6 | import { FastMCP } from 'fastmcp';
7 | import { LRUCache } from 'lru-cache';
8 |
9 | /**
10 | * Configuration options for the ContextManager
11 | * @typedef {Object} ContextManagerConfig
12 | * @property {number} maxCacheSize - Maximum number of items in the cache
13 | * @property {number} ttl - Time to live for cached items in milliseconds
14 | * @property {number} maxContextSize - Maximum size of context window in tokens
15 | */
16 |
17 | export class ContextManager {
18 | /**
19 | * Create a new ContextManager instance
20 | * @param {ContextManagerConfig} config - Configuration options
21 | */
22 | constructor(config = {}) {
23 | this.config = {
24 | maxCacheSize: config.maxCacheSize || 1000,
25 | ttl: config.ttl || 1000 * 60 * 5, // 5 minutes default
26 | maxContextSize: config.maxContextSize || 4000
27 | };
28 |
29 | // Initialize LRU cache for context data
30 | this.cache = new LRUCache({
31 | max: this.config.maxCacheSize,
32 | ttl: this.config.ttl,
33 | updateAgeOnGet: true
34 | });
35 |
36 | // Cache statistics
37 | this.stats = {
38 | hits: 0,
39 | misses: 0,
40 | invalidations: 0
41 | };
42 | }
43 |
44 | /**
45 | * Create a new context or retrieve from cache
46 | * @param {string} contextId - Unique identifier for the context
47 | * @param {Object} metadata - Additional metadata for the context
48 | * @returns {Object} Context object with metadata
49 | */
50 | async getContext(contextId, metadata = {}) {
51 | const cacheKey = this._getCacheKey(contextId, metadata);
52 |
53 | // Try to get from cache first
54 | const cached = this.cache.get(cacheKey);
55 | if (cached) {
56 | this.stats.hits++;
57 | return cached;
58 | }
59 |
60 | this.stats.misses++;
61 |
62 | // Create new context if not in cache
63 | const context = {
64 | id: contextId,
65 | metadata: {
66 | ...metadata,
67 | created: new Date().toISOString()
68 | }
69 | };
70 |
71 | // Cache the new context
72 | this.cache.set(cacheKey, context);
73 |
74 | return context;
75 | }
76 |
77 | /**
78 | * Update an existing context
79 | * @param {string} contextId - Context identifier
80 | * @param {Object} updates - Updates to apply to the context
81 | * @returns {Object} Updated context
82 | */
83 | async updateContext(contextId, updates) {
84 | const context = await this.getContext(contextId);
85 |
86 | // Apply updates to context
87 | Object.assign(context.metadata, updates);
88 |
89 | // Update cache
90 | const cacheKey = this._getCacheKey(contextId, context.metadata);
91 | this.cache.set(cacheKey, context);
92 |
93 | return context;
94 | }
95 |
96 | /**
97 | * Invalidate a context in the cache
98 | * @param {string} contextId - Context identifier
99 | * @param {Object} metadata - Metadata used in the cache key
100 | */
101 | invalidateContext(contextId, metadata = {}) {
102 | const cacheKey = this._getCacheKey(contextId, metadata);
103 | this.cache.delete(cacheKey);
104 | this.stats.invalidations++;
105 | }
106 |
107 | /**
108 | * Get cached data associated with a specific key.
109 | * Increments cache hit stats if found.
110 | * @param {string} key - The cache key.
111 | * @returns {any | undefined} The cached data or undefined if not found/expired.
112 | */
113 | getCachedData(key) {
114 | const cached = this.cache.get(key);
115 | if (cached !== undefined) {
116 | // Check for undefined specifically, as null/false might be valid cached values
117 | this.stats.hits++;
118 | return cached;
119 | }
120 | this.stats.misses++;
121 | return undefined;
122 | }
123 |
124 | /**
125 | * Set data in the cache with a specific key.
126 | * @param {string} key - The cache key.
127 | * @param {any} data - The data to cache.
128 | */
129 | setCachedData(key, data) {
130 | this.cache.set(key, data);
131 | }
132 |
133 | /**
134 | * Invalidate a specific cache key.
135 | * Increments invalidation stats.
136 | * @param {string} key - The cache key to invalidate.
137 | */
138 | invalidateCacheKey(key) {
139 | this.cache.delete(key);
140 | this.stats.invalidations++;
141 | }
142 |
143 | /**
144 | * Get cache statistics
145 | * @returns {Object} Cache statistics
146 | */
147 | getStats() {
148 | return {
149 | hits: this.stats.hits,
150 | misses: this.stats.misses,
151 | invalidations: this.stats.invalidations,
152 | size: this.cache.size,
153 | maxSize: this.config.maxCacheSize,
154 | ttl: this.config.ttl
155 | };
156 | }
157 |
158 | /**
159 | * Generate a cache key from context ID and metadata
160 | * @private
161 | * @deprecated No longer used for direct cache key generation outside the manager.
162 | * Prefer generating specific keys in calling functions.
163 | */
164 | _getCacheKey(contextId, metadata) {
165 | // Kept for potential backward compatibility or internal use if needed later.
166 | return `${contextId}:${JSON.stringify(metadata)}`;
167 | }
168 | }
169 |
170 | // Export a singleton instance with default config
171 | export const contextManager = new ContextManager();
172 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/git/services/commit-message-generator.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * CommitMessageGenerator - Generate conventional commit messages with metadata
3 | *
4 | * Combines TemplateEngine and ScopeDetector to create structured commit messages
5 | * that follow conventional commits specification and include task metadata.
6 | */
7 |
8 | import { ScopeDetector } from './scope-detector.js';
9 | import { TemplateEngine } from './template-engine.js';
10 |
11 | export interface CommitMessageOptions {
12 | type: string;
13 | description: string;
14 | changedFiles: string[];
15 | scope?: string;
16 | body?: string;
17 | breaking?: boolean;
18 | taskId?: string;
19 | phase?: string;
20 | tag?: string;
21 | testsPassing?: number;
22 | testsFailing?: number;
23 | coveragePercent?: number;
24 | }
25 |
26 | export interface ValidationResult {
27 | isValid: boolean;
28 | errors: string[];
29 | }
30 |
31 | export interface ParsedCommitMessage {
32 | type: string;
33 | scope?: string;
34 | breaking: boolean;
35 | description: string;
36 | body?: string;
37 | }
38 |
39 | const CONVENTIONAL_COMMIT_TYPES = [
40 | 'feat',
41 | 'fix',
42 | 'docs',
43 | 'style',
44 | 'refactor',
45 | 'perf',
46 | 'test',
47 | 'build',
48 | 'ci',
49 | 'chore',
50 | 'revert'
51 | ];
52 |
53 | export class CommitMessageGenerator {
54 | private templateEngine: TemplateEngine;
55 | private scopeDetector: ScopeDetector;
56 |
57 | constructor(
58 | customTemplates?: Record<string, string>,
59 | customScopeMappings?: Record<string, string>,
60 | customScopePriorities?: Record<string, number>
61 | ) {
62 | this.templateEngine = new TemplateEngine(customTemplates);
63 | this.scopeDetector = new ScopeDetector(
64 | customScopeMappings,
65 | customScopePriorities
66 | );
67 | }
68 |
69 | /**
70 | * Generate a conventional commit message with metadata
71 | */
72 | generateMessage(options: CommitMessageOptions): string {
73 | const {
74 | type,
75 | description,
76 | changedFiles,
77 | scope: manualScope,
78 | body,
79 | breaking = false,
80 | taskId,
81 | phase,
82 | tag,
83 | testsPassing,
84 | testsFailing,
85 | coveragePercent
86 | } = options;
87 |
88 | // Determine scope (manual override or auto-detect)
89 | const scope = manualScope ?? this.scopeDetector.detectScope(changedFiles);
90 |
91 | // Build template variables
92 | const variables = {
93 | type,
94 | scope,
95 | breaking: breaking ? '!' : '',
96 | description,
97 | body,
98 | taskId,
99 | phase,
100 | tag,
101 | testsPassing,
102 | testsFailing,
103 | coveragePercent
104 | };
105 |
106 | // Generate message from template
107 | return this.templateEngine.render('commitMessage', variables);
108 | }
109 |
110 | /**
111 | * Validate that a commit message follows conventional commits format
112 | */
113 | validateConventionalCommit(message: string): ValidationResult {
114 | const errors: string[] = [];
115 |
116 | // Parse first line (header)
117 | const lines = message.split('\n');
118 | const header = lines[0];
119 |
120 | if (!header) {
121 | errors.push('Missing commit message');
122 | return { isValid: false, errors };
123 | }
124 |
125 | // Check format: type(scope)?: description
126 | const headerRegex = /^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/;
127 | const match = header.match(headerRegex);
128 |
129 | if (!match) {
130 | errors.push(
131 | 'Invalid conventional commit format. Expected: type(scope): description'
132 | );
133 | return { isValid: false, errors };
134 | }
135 |
136 | const [, type, , , description] = match;
137 |
138 | // Validate type
139 | if (!CONVENTIONAL_COMMIT_TYPES.includes(type)) {
140 | errors.push(
141 | `Invalid commit type "${type}". Must be one of: ${CONVENTIONAL_COMMIT_TYPES.join(', ')}`
142 | );
143 | }
144 |
145 | // Validate description
146 | if (!description || description.trim().length === 0) {
147 | errors.push('Missing description');
148 | }
149 |
150 | return {
151 | isValid: errors.length === 0,
152 | errors
153 | };
154 | }
155 |
156 | /**
157 | * Parse a conventional commit message into its components
158 | */
159 | parseCommitMessage(message: string): ParsedCommitMessage {
160 | const lines = message.split('\n');
161 | const header = lines[0];
162 |
163 | // Parse header: type(scope)!: description
164 | const headerRegex = /^(\w+)(?:\(([^)]+)\))?(!)?:\s*(.+)$/;
165 | const match = header.match(headerRegex);
166 |
167 | if (!match) {
168 | throw new Error('Invalid conventional commit format');
169 | }
170 |
171 | const [, type, scope, breaking, description] = match;
172 |
173 | // Body is everything after the first blank line
174 | const bodyStartIndex = lines.findIndex((line, i) => i > 0 && line === '');
175 | const body =
176 | bodyStartIndex !== -1
177 | ? lines
178 | .slice(bodyStartIndex + 1)
179 | .join('\n')
180 | .trim()
181 | : undefined;
182 |
183 | return {
184 | type,
185 | scope,
186 | breaking: breaking === '!',
187 | description,
188 | body
189 | };
190 | }
191 |
192 | /**
193 | * Get the scope detector instance (for testing/customization)
194 | */
195 | getScopeDetector(): ScopeDetector {
196 | return this.scopeDetector;
197 | }
198 |
199 | /**
200 | * Get the template engine instance (for testing/customization)
201 | */
202 | getTemplateEngine(): TemplateEngine {
203 | return this.templateEngine;
204 | }
205 | }
206 |
```
--------------------------------------------------------------------------------
/.github/scripts/parse-metrics.mjs:
--------------------------------------------------------------------------------
```
1 | #!/usr/bin/env node
2 |
3 | import { readFileSync, existsSync, writeFileSync } from 'fs';
4 |
5 | function parseMetricsTable(content, metricName) {
6 | const lines = content.split('\n');
7 |
8 | for (let i = 0; i < lines.length; i++) {
9 | const line = lines[i].trim();
10 | // Match a markdown table row like: | Metric Name | value | ...
11 | const safeName = metricName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12 | const re = new RegExp(`^\\|\\s*${safeName}\\s*\\|\\s*([^|]+)\\|?`);
13 | const match = line.match(re);
14 | if (match) {
15 | return match[1].trim() || 'N/A';
16 | }
17 | }
18 | return 'N/A';
19 | }
20 |
21 | function parseCountMetric(content, metricName) {
22 | const result = parseMetricsTable(content, metricName);
23 | // Extract number from string, handling commas and spaces
24 | const numberMatch = result.toString().match(/[\d,]+/);
25 | if (numberMatch) {
26 | const number = parseInt(numberMatch[0].replace(/,/g, ''));
27 | return isNaN(number) ? 0 : number;
28 | }
29 | return 0;
30 | }
31 |
32 | function main() {
33 | const metrics = {
34 | issues_created: 0,
35 | issues_closed: 0,
36 | prs_created: 0,
37 | prs_merged: 0,
38 | issue_avg_first_response: 'N/A',
39 | issue_avg_time_to_close: 'N/A',
40 | pr_avg_first_response: 'N/A',
41 | pr_avg_merge_time: 'N/A'
42 | };
43 |
44 | // Parse issue metrics
45 | if (existsSync('issue_metrics.md')) {
46 | console.log('📄 Found issue_metrics.md, parsing...');
47 | const issueContent = readFileSync('issue_metrics.md', 'utf8');
48 |
49 | metrics.issues_created = parseCountMetric(
50 | issueContent,
51 | 'Total number of items created'
52 | );
53 | metrics.issues_closed = parseCountMetric(
54 | issueContent,
55 | 'Number of items closed'
56 | );
57 | metrics.issue_avg_first_response = parseMetricsTable(
58 | issueContent,
59 | 'Time to first response'
60 | );
61 | metrics.issue_avg_time_to_close = parseMetricsTable(
62 | issueContent,
63 | 'Time to close'
64 | );
65 | } else {
66 | console.warn('[parse-metrics] issue_metrics.md not found; using defaults.');
67 | }
68 |
69 | // Parse PR created metrics
70 | if (existsSync('pr_created_metrics.md')) {
71 | console.log('📄 Found pr_created_metrics.md, parsing...');
72 | const prCreatedContent = readFileSync('pr_created_metrics.md', 'utf8');
73 |
74 | metrics.prs_created = parseCountMetric(
75 | prCreatedContent,
76 | 'Total number of items created'
77 | );
78 | metrics.pr_avg_first_response = parseMetricsTable(
79 | prCreatedContent,
80 | 'Time to first response'
81 | );
82 | } else {
83 | console.warn(
84 | '[parse-metrics] pr_created_metrics.md not found; using defaults.'
85 | );
86 | }
87 |
88 | // Parse PR merged metrics (for more accurate merge data)
89 | if (existsSync('pr_merged_metrics.md')) {
90 | console.log('📄 Found pr_merged_metrics.md, parsing...');
91 | const prMergedContent = readFileSync('pr_merged_metrics.md', 'utf8');
92 |
93 | metrics.prs_merged = parseCountMetric(
94 | prMergedContent,
95 | 'Total number of items created'
96 | );
97 | // For merged PRs, "Time to close" is actually time to merge
98 | metrics.pr_avg_merge_time = parseMetricsTable(
99 | prMergedContent,
100 | 'Time to close'
101 | );
102 | } else {
103 | console.warn(
104 | '[parse-metrics] pr_merged_metrics.md not found; falling back to pr_metrics.md.'
105 | );
106 | // Fallback: try old pr_metrics.md if it exists
107 | if (existsSync('pr_metrics.md')) {
108 | console.log('📄 Falling back to pr_metrics.md...');
109 | const prContent = readFileSync('pr_metrics.md', 'utf8');
110 |
111 | const mergedCount = parseCountMetric(prContent, 'Number of items merged');
112 | metrics.prs_merged =
113 | mergedCount || parseCountMetric(prContent, 'Number of items closed');
114 |
115 | const maybeMergeTime = parseMetricsTable(
116 | prContent,
117 | 'Average time to merge'
118 | );
119 | metrics.pr_avg_merge_time =
120 | maybeMergeTime !== 'N/A'
121 | ? maybeMergeTime
122 | : parseMetricsTable(prContent, 'Time to close');
123 | } else {
124 | console.warn('[parse-metrics] pr_metrics.md not found; using defaults.');
125 | }
126 | }
127 |
128 | // Output for GitHub Actions
129 | const output = Object.entries(metrics)
130 | .map(([key, value]) => `${key}=${value}`)
131 | .join('\n');
132 |
133 | // Always output to stdout for debugging
134 | console.log('\n=== FINAL METRICS ===');
135 | Object.entries(metrics).forEach(([key, value]) => {
136 | console.log(`${key}: ${value}`);
137 | });
138 |
139 | // Write to GITHUB_OUTPUT if in GitHub Actions
140 | if (process.env.GITHUB_OUTPUT) {
141 | try {
142 | writeFileSync(process.env.GITHUB_OUTPUT, output + '\n', { flag: 'a' });
143 | console.log(
144 | `\nSuccessfully wrote metrics to ${process.env.GITHUB_OUTPUT}`
145 | );
146 | } catch (error) {
147 | console.error(`Failed to write to GITHUB_OUTPUT: ${error.message}`);
148 | process.exit(1);
149 | }
150 | } else {
151 | console.log(
152 | '\nNo GITHUB_OUTPUT environment variable found, skipping file write'
153 | );
154 | }
155 | }
156 |
157 | main();
158 |
```
--------------------------------------------------------------------------------
/apps/extension/src/webview/components/ToastNotification.tsx:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Toast Notification Component
3 | */
4 |
5 | import React, { useState, useEffect } from 'react';
6 | import type { ToastNotification as ToastType } from '../types';
7 |
8 | interface ToastNotificationProps {
9 | notification: ToastType;
10 | onDismiss: (id: string) => void;
11 | }
12 |
13 | export const ToastNotification: React.FC<ToastNotificationProps> = ({
14 | notification,
15 | onDismiss
16 | }) => {
17 | const [isVisible, setIsVisible] = useState(true);
18 | const [progress, setProgress] = useState(100);
19 | const duration = notification.duration || 5000; // 5 seconds default
20 |
21 | useEffect(() => {
22 | const progressInterval = setInterval(() => {
23 | setProgress((prev) => {
24 | const decrease = (100 / duration) * 100; // Update every 100ms
25 | return Math.max(0, prev - decrease);
26 | });
27 | }, 100);
28 |
29 | const timeoutId = setTimeout(() => {
30 | setIsVisible(false);
31 | setTimeout(() => onDismiss(notification.id), 300); // Wait for animation
32 | }, duration);
33 |
34 | return () => {
35 | clearInterval(progressInterval);
36 | clearTimeout(timeoutId);
37 | };
38 | }, [notification.id, duration, onDismiss]);
39 |
40 | const getIcon = () => {
41 | switch (notification.type) {
42 | case 'success':
43 | return (
44 | <svg
45 | className="w-5 h-5 text-green-400"
46 | fill="none"
47 | stroke="currentColor"
48 | viewBox="0 0 24 24"
49 | >
50 | <path
51 | strokeLinecap="round"
52 | strokeLinejoin="round"
53 | strokeWidth={2}
54 | d="M5 13l4 4L19 7"
55 | />
56 | </svg>
57 | );
58 | case 'info':
59 | return (
60 | <svg
61 | className="w-5 h-5 text-blue-400"
62 | fill="none"
63 | stroke="currentColor"
64 | viewBox="0 0 24 24"
65 | >
66 | <path
67 | strokeLinecap="round"
68 | strokeLinejoin="round"
69 | strokeWidth={2}
70 | d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
71 | />
72 | </svg>
73 | );
74 | case 'warning':
75 | return (
76 | <svg
77 | className="w-5 h-5 text-yellow-400"
78 | fill="none"
79 | stroke="currentColor"
80 | viewBox="0 0 24 24"
81 | >
82 | <path
83 | strokeLinecap="round"
84 | strokeLinejoin="round"
85 | strokeWidth={2}
86 | d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.667-2.308-1.667-3.08 0L3.34 19c-.77 1.333.192 3 1.732 3z"
87 | />
88 | </svg>
89 | );
90 | case 'error':
91 | return (
92 | <svg
93 | className="w-5 h-5 text-red-400"
94 | fill="none"
95 | stroke="currentColor"
96 | viewBox="0 0 24 24"
97 | >
98 | <path
99 | strokeLinecap="round"
100 | strokeLinejoin="round"
101 | strokeWidth={2}
102 | d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
103 | />
104 | </svg>
105 | );
106 | }
107 | };
108 |
109 | const bgColor = {
110 | success: 'bg-green-900/90',
111 | info: 'bg-blue-900/90',
112 | warning: 'bg-yellow-900/90',
113 | error: 'bg-red-900/90'
114 | }[notification.type];
115 |
116 | const borderColor = {
117 | success: 'border-green-600',
118 | info: 'border-blue-600',
119 | warning: 'border-yellow-600',
120 | error: 'border-red-600'
121 | }[notification.type];
122 |
123 | const progressColor = {
124 | success: 'bg-green-400',
125 | info: 'bg-blue-400',
126 | warning: 'bg-yellow-400',
127 | error: 'bg-red-400'
128 | }[notification.type];
129 |
130 | return (
131 | <div
132 | className={`${bgColor} ${borderColor} border rounded-lg shadow-lg p-4 mb-2 transition-all duration-300 ${
133 | isVisible ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-full'
134 | } max-w-sm w-full relative overflow-hidden`}
135 | >
136 | <div className="flex items-start">
137 | <div className="flex-shrink-0">{getIcon()}</div>
138 | <div className="ml-3 flex-1">
139 | <h3 className="text-sm font-medium text-white">
140 | {notification.title}
141 | </h3>
142 | <p className="mt-1 text-sm text-gray-300">{notification.message}</p>
143 | </div>
144 | <button
145 | onClick={() => onDismiss(notification.id)}
146 | className="ml-4 flex-shrink-0 inline-flex text-gray-400 hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white"
147 | >
148 | <span className="sr-only">Close</span>
149 | <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
150 | <path
151 | fillRule="evenodd"
152 | d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
153 | clipRule="evenodd"
154 | />
155 | </svg>
156 | </button>
157 | </div>
158 | {/* Progress bar */}
159 | <div className="absolute bottom-0 left-0 w-full h-1 bg-gray-700">
160 | <div
161 | className={`h-full ${progressColor} transition-all duration-100 ease-linear`}
162 | style={{ width: `${progress}%` }}
163 | />
164 | </div>
165 | </div>
166 | );
167 | };
168 |
```
--------------------------------------------------------------------------------
/apps/extension/esbuild.js:
--------------------------------------------------------------------------------
```javascript
1 | const esbuild = require('esbuild');
2 | const path = require('path');
3 |
4 | const production = process.argv.includes('--production');
5 | const watch = process.argv.includes('--watch');
6 |
7 | /**
8 | * @type {import('esbuild').Plugin}
9 | */
10 | const esbuildProblemMatcherPlugin = {
11 | name: 'esbuild-problem-matcher',
12 |
13 | setup(build) {
14 | build.onStart(() => {
15 | console.log('[watch] build started');
16 | });
17 | build.onEnd((result) => {
18 | result.errors.forEach(({ text, location }) => {
19 | console.error(`✘ [ERROR] ${text}`);
20 | console.error(
21 | ` ${location.file}:${location.line}:${location.column}:`
22 | );
23 | });
24 | console.log('[watch] build finished');
25 | });
26 | }
27 | };
28 |
29 | /**
30 | * @type {import('esbuild').Plugin}
31 | */
32 | const aliasPlugin = {
33 | name: 'alias',
34 | setup(build) {
35 | // Handle @/ aliases for shadcn/ui
36 | build.onResolve({ filter: /^@\// }, (args) => {
37 | const resolvedPath = path.resolve(__dirname, 'src', args.path.slice(2));
38 |
39 | // Try to resolve with common TypeScript extensions
40 | const fs = require('fs');
41 | const extensions = ['.tsx', '.ts', '.jsx', '.js'];
42 |
43 | // Check if it's a file first
44 | for (const ext of extensions) {
45 | const fullPath = resolvedPath + ext;
46 | if (fs.existsSync(fullPath)) {
47 | return { path: fullPath };
48 | }
49 | }
50 |
51 | // Check if it's a directory with index file
52 | for (const ext of extensions) {
53 | const indexPath = path.join(resolvedPath, 'index' + ext);
54 | if (fs.existsSync(indexPath)) {
55 | return { path: indexPath };
56 | }
57 | }
58 |
59 | // Fallback to original behavior
60 | return { path: resolvedPath };
61 | });
62 | }
63 | };
64 |
65 | async function main() {
66 | // Build configuration for the VS Code extension
67 | const extensionCtx = await esbuild.context({
68 | entryPoints: ['src/extension.ts'],
69 | bundle: true,
70 | format: 'cjs',
71 | minify: production,
72 | sourcemap: !production ? 'inline' : false,
73 | sourcesContent: !production,
74 | platform: 'node',
75 | outdir: 'dist',
76 | external: ['vscode'],
77 | logLevel: 'silent',
78 | // Add production optimizations
79 | ...(production && {
80 | drop: ['debugger'],
81 | pure: ['console.log', 'console.debug', 'console.trace']
82 | }),
83 | plugins: [esbuildProblemMatcherPlugin, aliasPlugin]
84 | });
85 |
86 | // Build configuration for the React webview
87 | const webviewCtx = await esbuild.context({
88 | entryPoints: ['src/webview/index.tsx'],
89 | bundle: true,
90 | format: 'iife',
91 | globalName: 'App',
92 | minify: production,
93 | sourcemap: !production ? 'inline' : false,
94 | sourcesContent: !production,
95 | platform: 'browser',
96 | outdir: 'dist',
97 | logLevel: 'silent',
98 | target: ['es2020'],
99 | jsx: 'automatic',
100 | jsxImportSource: 'react',
101 | external: ['*.css'],
102 | // Bundle React with webview since it's not available in the runtime
103 | // This prevents the multiple React instances issue
104 | // Ensure React is resolved from the workspace root to avoid duplicates
105 | alias: {
106 | react: path.resolve(__dirname, '../../node_modules/react'),
107 | 'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
108 | },
109 | define: {
110 | 'process.env.NODE_ENV': production ? '"production"' : '"development"',
111 | global: 'globalThis'
112 | },
113 | // Add production optimizations for webview too
114 | ...(production && {
115 | drop: ['debugger'],
116 | pure: ['console.log', 'console.debug', 'console.trace']
117 | }),
118 | plugins: [esbuildProblemMatcherPlugin, aliasPlugin]
119 | });
120 |
121 | // Build configuration for the React sidebar
122 | const sidebarCtx = await esbuild.context({
123 | entryPoints: ['src/webview/sidebar.tsx'],
124 | bundle: true,
125 | format: 'iife',
126 | globalName: 'SidebarApp',
127 | minify: production,
128 | sourcemap: !production ? 'inline' : false,
129 | sourcesContent: !production,
130 | platform: 'browser',
131 | outdir: 'dist',
132 | logLevel: 'silent',
133 | target: ['es2020'],
134 | jsx: 'automatic',
135 | jsxImportSource: 'react',
136 | external: ['*.css'],
137 | alias: {
138 | react: path.resolve(__dirname, '../../node_modules/react'),
139 | 'react-dom': path.resolve(__dirname, '../../node_modules/react-dom')
140 | },
141 | define: {
142 | 'process.env.NODE_ENV': production ? '"production"' : '"development"',
143 | global: 'globalThis'
144 | },
145 | ...(production && {
146 | drop: ['debugger'],
147 | pure: ['console.log', 'console.debug', 'console.trace']
148 | }),
149 | plugins: [esbuildProblemMatcherPlugin, aliasPlugin]
150 | });
151 |
152 | if (watch) {
153 | await Promise.all([
154 | extensionCtx.watch(),
155 | webviewCtx.watch(),
156 | sidebarCtx.watch()
157 | ]);
158 | } else {
159 | await Promise.all([
160 | extensionCtx.rebuild(),
161 | webviewCtx.rebuild(),
162 | sidebarCtx.rebuild()
163 | ]);
164 | await extensionCtx.dispose();
165 | await webviewCtx.dispose();
166 | await sidebarCtx.dispose();
167 | }
168 | }
169 |
170 | main().catch((e) => {
171 | console.error(e);
172 | process.exit(1);
173 | });
174 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/setup.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Common setup for task-manager module tests
3 | */
4 | import { jest } from '@jest/globals';
5 |
6 | // Sample test data
7 | export const sampleTasks = {
8 | meta: { projectName: 'Test Project' },
9 | tasks: [
10 | {
11 | id: 1,
12 | title: 'Task 1',
13 | description: 'First task description',
14 | status: 'pending',
15 | dependencies: [],
16 | priority: 'high',
17 | details: 'Detailed information for task 1',
18 | testStrategy: 'Test strategy for task 1'
19 | },
20 | {
21 | id: 2,
22 | title: 'Task 2',
23 | description: 'Second task description',
24 | status: 'pending',
25 | dependencies: [1],
26 | priority: 'medium',
27 | details: 'Detailed information for task 2',
28 | testStrategy: 'Test strategy for task 2'
29 | },
30 | {
31 | id: 3,
32 | title: 'Task with Subtasks',
33 | description: 'Task with subtasks description',
34 | status: 'pending',
35 | dependencies: [1, 2],
36 | priority: 'high',
37 | details: 'Detailed information for task 3',
38 | testStrategy: 'Test strategy for task 3',
39 | subtasks: [
40 | {
41 | id: 1,
42 | title: 'Subtask 1',
43 | description: 'First subtask',
44 | status: 'pending',
45 | dependencies: [],
46 | details: 'Details for subtask 1'
47 | },
48 | {
49 | id: 2,
50 | title: 'Subtask 2',
51 | description: 'Second subtask',
52 | status: 'pending',
53 | dependencies: [1],
54 | details: 'Details for subtask 2'
55 | }
56 | ]
57 | }
58 | ]
59 | };
60 |
61 | export const emptySampleTasks = {
62 | meta: { projectName: 'Empty Project' },
63 | tasks: []
64 | };
65 |
66 | export const sampleClaudeResponse = {
67 | tasks: [
68 | {
69 | id: 1,
70 | title: 'Setup Project',
71 | description: 'Initialize the project structure',
72 | status: 'pending',
73 | dependencies: [],
74 | priority: 'high',
75 | details:
76 | 'Create repository, configure build system, and setup dev environment',
77 | testStrategy: 'Verify project builds and tests run'
78 | },
79 | {
80 | id: 2,
81 | title: 'Implement Core Feature',
82 | description: 'Create the main functionality',
83 | status: 'pending',
84 | dependencies: [1],
85 | priority: 'high',
86 | details: 'Implement the core business logic for the application',
87 | testStrategy:
88 | 'Unit tests for core functions, integration tests for workflows'
89 | }
90 | ]
91 | };
92 |
93 | // Common mock setup function
94 | export const setupCommonMocks = () => {
95 | // Clear mocks before setup
96 | jest.clearAllMocks();
97 |
98 | // Mock implementations
99 | const mocks = {
100 | readFileSync: jest.fn(),
101 | existsSync: jest.fn(),
102 | mkdirSync: jest.fn(),
103 | writeFileSync: jest.fn(),
104 | readJSON: jest.fn(),
105 | writeJSON: jest.fn(),
106 | log: jest.fn(),
107 | isTaskDependentOn: jest.fn().mockReturnValue(false),
108 | formatDependenciesWithStatus: jest.fn(),
109 | displayTaskList: jest.fn(),
110 | validateAndFixDependencies: jest.fn(),
111 | generateObjectService: jest.fn().mockResolvedValue({
112 | mainResult: { tasks: [] },
113 | telemetryData: {}
114 | })
115 | };
116 |
117 | return mocks;
118 | };
119 |
120 | // Helper to create a deep copy of objects to avoid test pollution
121 | export const cloneData = (data) => JSON.parse(JSON.stringify(data));
122 |
123 | /**
124 | * Shared mock implementation for getTagAwareFilePath that matches the actual implementation
125 | * This ensures consistent behavior across all test files, particularly regarding projectRoot handling.
126 | *
127 | * The key difference from previous inconsistent implementations was that some tests were not
128 | * properly handling the projectRoot parameter, leading to different behaviors between test files.
129 | *
130 | * @param {string} basePath - The base file path
131 | * @param {string|null} tag - The tag name (null, undefined, or 'master' uses base path)
132 | * @param {string} [projectRoot='.'] - The project root directory
133 | * @returns {string} The resolved file path
134 | */
135 | export const createGetTagAwareFilePathMock = () => {
136 | return jest.fn((basePath, tag, projectRoot = '.') => {
137 | // Handle projectRoot consistently - this was the key fix
138 | const fullPath = projectRoot ? `${projectRoot}/${basePath}` : basePath;
139 |
140 | if (!tag || tag === 'master') {
141 | return fullPath;
142 | }
143 |
144 | // Mock the slugification behavior (matches actual implementation)
145 | const slugifiedTag = tag.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
146 | const idx = fullPath.lastIndexOf('.');
147 | return `${fullPath.slice(0, idx)}_${slugifiedTag}${fullPath.slice(idx)}`;
148 | });
149 | };
150 |
151 | /**
152 | * Shared mock implementation for slugifyTagForFilePath that matches the actual implementation
153 | * @param {string} tagName - The tag name to slugify
154 | * @returns {string} Slugified tag name safe for filesystem use
155 | */
156 | export const createSlugifyTagForFilePathMock = () => {
157 | return jest.fn((tagName) => {
158 | if (!tagName || typeof tagName !== 'string') {
159 | return 'unknown-tag';
160 | }
161 | return tagName.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase();
162 | });
163 | };
164 |
```
--------------------------------------------------------------------------------
/tests/unit/profiles/gemini-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('Gemini Profile 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('AGENTS.md')) {
33 | return 'Sample AGENTS.md content for Gemini integration';
34 | }
35 | return '{}';
36 | });
37 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false);
38 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
39 | });
40 |
41 | afterEach(() => {
42 | // Clean up the temporary directory
43 | try {
44 | fs.rmSync(tempDir, { recursive: true, force: true });
45 | } catch (err) {
46 | console.error(`Error cleaning up: ${err.message}`);
47 | }
48 | });
49 |
50 | // Test function that simulates the Gemini profile file copying behavior
51 | function mockCreateGeminiStructure() {
52 | // Gemini profile copies AGENTS.md to GEMINI.md in project root
53 | const sourceContent = 'Sample AGENTS.md content for Gemini integration';
54 | fs.writeFileSync(path.join(tempDir, 'GEMINI.md'), sourceContent);
55 |
56 | // Gemini profile creates .gemini directory
57 | fs.mkdirSync(path.join(tempDir, '.gemini'), { recursive: true });
58 |
59 | // Gemini profile creates settings.json in .gemini directory
60 | const settingsContent = JSON.stringify(
61 | {
62 | mcpServers: {
63 | 'task-master-ai': {
64 | command: 'npx',
65 | args: ['-y', 'task-master-ai'],
66 | env: {
67 | YOUR_ANTHROPIC_API_KEY: 'your-api-key-here',
68 | YOUR_PERPLEXITY_API_KEY: 'your-api-key-here',
69 | YOUR_OPENAI_API_KEY: 'your-api-key-here',
70 | YOUR_GOOGLE_API_KEY: 'your-api-key-here',
71 | YOUR_MISTRAL_API_KEY: 'your-api-key-here',
72 | YOUR_AZURE_OPENAI_API_KEY: 'your-api-key-here',
73 | YOUR_AZURE_OPENAI_ENDPOINT: 'your-endpoint-here',
74 | YOUR_OPENROUTER_API_KEY: 'your-api-key-here',
75 | YOUR_XAI_API_KEY: 'your-api-key-here',
76 | YOUR_OLLAMA_API_KEY: 'your-api-key-here',
77 | YOUR_OLLAMA_BASE_URL: 'http://localhost:11434/api',
78 | YOUR_AWS_ACCESS_KEY_ID: 'your-access-key-id',
79 | YOUR_AWS_SECRET_ACCESS_KEY: 'your-secret-access-key',
80 | YOUR_AWS_REGION: 'us-east-1'
81 | }
82 | }
83 | }
84 | },
85 | null,
86 | 2
87 | );
88 | fs.writeFileSync(
89 | path.join(tempDir, '.gemini', 'settings.json'),
90 | settingsContent
91 | );
92 | }
93 |
94 | test('creates GEMINI.md file in project root', () => {
95 | // Act
96 | mockCreateGeminiStructure();
97 |
98 | // Assert
99 | expect(fs.writeFileSync).toHaveBeenCalledWith(
100 | path.join(tempDir, 'GEMINI.md'),
101 | 'Sample AGENTS.md content for Gemini integration'
102 | );
103 | });
104 |
105 | test('creates .gemini profile directory', () => {
106 | // Act
107 | mockCreateGeminiStructure();
108 |
109 | // Assert
110 | expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.gemini'), {
111 | recursive: true
112 | });
113 | });
114 |
115 | test('creates MCP configuration as settings.json', () => {
116 | // Act
117 | mockCreateGeminiStructure();
118 |
119 | // Assert - Gemini profile should create settings.json instead of mcp.json
120 | const writeFileCalls = fs.writeFileSync.mock.calls;
121 | const settingsJsonCall = writeFileCalls.find((call) =>
122 | call[0].toString().includes('.gemini/settings.json')
123 | );
124 | expect(settingsJsonCall).toBeDefined();
125 | });
126 |
127 | test('uses settings.json instead of mcp.json', () => {
128 | // Act
129 | mockCreateGeminiStructure();
130 |
131 | // Assert - Should use settings.json, not mcp.json
132 | const writeFileCalls = fs.writeFileSync.mock.calls;
133 | const mcpJsonCalls = writeFileCalls.filter((call) =>
134 | call[0].toString().includes('mcp.json')
135 | );
136 | expect(mcpJsonCalls).toHaveLength(0);
137 |
138 | const settingsJsonCalls = writeFileCalls.filter((call) =>
139 | call[0].toString().includes('settings.json')
140 | );
141 | expect(settingsJsonCalls).toHaveLength(1);
142 | });
143 |
144 | test('renames AGENTS.md to GEMINI.md', () => {
145 | // Act
146 | mockCreateGeminiStructure();
147 |
148 | // Assert - Gemini should rename AGENTS.md to GEMINI.md
149 | const writeFileCalls = fs.writeFileSync.mock.calls;
150 | const geminiMdCall = writeFileCalls.find((call) =>
151 | call[0].toString().includes('GEMINI.md')
152 | );
153 | expect(geminiMdCall).toBeDefined();
154 | expect(geminiMdCall[0]).toBe(path.join(tempDir, 'GEMINI.md'));
155 | });
156 | });
157 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/update-single-task-status.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Tests for the updateSingleTaskStatus function
3 | */
4 | import { jest } from '@jest/globals';
5 |
6 | // Import test fixtures
7 | import {
8 | isValidTaskStatus,
9 | TASK_STATUS_OPTIONS
10 | } from '../../../../../src/constants/task-status.js';
11 |
12 | // Sample tasks data for testing
13 | const sampleTasks = {
14 | tasks: [
15 | {
16 | id: 1,
17 | title: 'Task 1',
18 | description: 'First task',
19 | status: 'pending',
20 | dependencies: []
21 | },
22 | {
23 | id: 2,
24 | title: 'Task 2',
25 | description: 'Second task',
26 | status: 'pending',
27 | dependencies: []
28 | },
29 | {
30 | id: 3,
31 | title: 'Task 3',
32 | description: 'Third task with subtasks',
33 | status: 'pending',
34 | dependencies: [],
35 | subtasks: [
36 | {
37 | id: 1,
38 | title: 'Subtask 3.1',
39 | description: 'First subtask',
40 | status: 'pending',
41 | dependencies: []
42 | },
43 | {
44 | id: 2,
45 | title: 'Subtask 3.2',
46 | description: 'Second subtask',
47 | status: 'pending',
48 | dependencies: []
49 | }
50 | ]
51 | }
52 | ]
53 | };
54 |
55 | // Simplified version of updateSingleTaskStatus for testing
56 | const testUpdateSingleTaskStatus = (tasksData, taskIdInput, newStatus) => {
57 | if (!isValidTaskStatus(newStatus)) {
58 | throw new Error(
59 | `Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
60 | );
61 | }
62 |
63 | // Check if it's a subtask (e.g., "1.2")
64 | if (taskIdInput.includes('.')) {
65 | const [parentId, subtaskId] = taskIdInput
66 | .split('.')
67 | .map((id) => parseInt(id, 10));
68 |
69 | // Find the parent task
70 | const parentTask = tasksData.tasks.find((t) => t.id === parentId);
71 | if (!parentTask) {
72 | throw new Error(`Parent task ${parentId} not found`);
73 | }
74 |
75 | // Find the subtask
76 | if (!parentTask.subtasks) {
77 | throw new Error(`Parent task ${parentId} has no subtasks`);
78 | }
79 |
80 | const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
81 | if (!subtask) {
82 | throw new Error(
83 | `Subtask ${subtaskId} not found in parent task ${parentId}`
84 | );
85 | }
86 |
87 | // Update the subtask status
88 | subtask.status = newStatus;
89 |
90 | // Check if all subtasks are done (if setting to 'done')
91 | if (
92 | newStatus.toLowerCase() === 'done' ||
93 | newStatus.toLowerCase() === 'completed'
94 | ) {
95 | const allSubtasksDone = parentTask.subtasks.every(
96 | (st) => st.status === 'done' || st.status === 'completed'
97 | );
98 |
99 | // For testing, we don't need to output suggestions
100 | }
101 | } else {
102 | // Handle regular task
103 | const taskId = parseInt(taskIdInput, 10);
104 | const task = tasksData.tasks.find((t) => t.id === taskId);
105 |
106 | if (!task) {
107 | throw new Error(`Task ${taskId} not found`);
108 | }
109 |
110 | // Update the task status
111 | task.status = newStatus;
112 |
113 | // If marking as done, also mark all subtasks as done
114 | if (
115 | (newStatus.toLowerCase() === 'done' ||
116 | newStatus.toLowerCase() === 'completed') &&
117 | task.subtasks &&
118 | task.subtasks.length > 0
119 | ) {
120 | task.subtasks.forEach((subtask) => {
121 | subtask.status = newStatus;
122 | });
123 | }
124 | }
125 |
126 | return true;
127 | };
128 |
129 | describe('updateSingleTaskStatus function', () => {
130 | test('should update regular task status', async () => {
131 | // Arrange
132 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
133 |
134 | // Act
135 | const result = testUpdateSingleTaskStatus(testTasksData, '2', 'done');
136 |
137 | // Assert
138 | expect(result).toBe(true);
139 | expect(testTasksData.tasks[1].status).toBe('done');
140 | });
141 |
142 | test('should throw error for invalid status', async () => {
143 | // Arrange
144 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
145 |
146 | // Assert
147 | expect(() =>
148 | testUpdateSingleTaskStatus(testTasksData, '2', 'Done')
149 | ).toThrow(/Error: Invalid status value: Done./);
150 | });
151 |
152 | test('should update subtask status', async () => {
153 | // Arrange
154 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
155 |
156 | // Act
157 | const result = testUpdateSingleTaskStatus(testTasksData, '3.1', 'done');
158 |
159 | // Assert
160 | expect(result).toBe(true);
161 | expect(testTasksData.tasks[2].subtasks[0].status).toBe('done');
162 | });
163 |
164 | test('should handle parent tasks without subtasks', async () => {
165 | // Arrange
166 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
167 |
168 | // Remove subtasks from task 3
169 | const taskWithoutSubtasks = { ...testTasksData.tasks[2] };
170 | delete taskWithoutSubtasks.subtasks;
171 | testTasksData.tasks[2] = taskWithoutSubtasks;
172 |
173 | // Assert
174 | expect(() =>
175 | testUpdateSingleTaskStatus(testTasksData, '3.1', 'done')
176 | ).toThrow('has no subtasks');
177 | });
178 |
179 | test('should handle non-existent subtask ID', async () => {
180 | // Arrange
181 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
182 |
183 | // Assert
184 | expect(() =>
185 | testUpdateSingleTaskStatus(testTasksData, '3.99', 'done')
186 | ).toThrow('Subtask 99 not found');
187 | });
188 | });
189 |
```
--------------------------------------------------------------------------------
/packages/tm-core/tests/auth/auth-refresh.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fs from 'fs';
2 | import os from 'os';
3 | import path from 'path';
4 | import type { Session } from '@supabase/supabase-js';
5 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6 | import { AuthManager } from '../../src/auth/auth-manager';
7 | import { CredentialStore } from '../../src/auth/credential-store';
8 | import type { AuthCredentials } from '../../src/auth/types';
9 |
10 | describe('AuthManager Token Refresh', () => {
11 | let authManager: AuthManager;
12 | let credentialStore: CredentialStore;
13 | let tmpDir: string;
14 | let authFile: string;
15 |
16 | beforeEach(() => {
17 | // Reset singletons
18 | AuthManager.resetInstance();
19 | CredentialStore.resetInstance();
20 |
21 | // Create temporary directory for test isolation
22 | tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-auth-refresh-'));
23 | authFile = path.join(tmpDir, 'auth.json');
24 |
25 | // Initialize AuthManager with test config (this will create CredentialStore internally)
26 | authManager = AuthManager.getInstance({
27 | configDir: tmpDir,
28 | configFile: authFile
29 | });
30 |
31 | // Get the CredentialStore instance that AuthManager created
32 | credentialStore = CredentialStore.getInstance();
33 | credentialStore.clearCredentials();
34 | });
35 |
36 | afterEach(() => {
37 | // Clean up
38 | try {
39 | credentialStore.clearCredentials();
40 | } catch {
41 | // Ignore cleanup errors
42 | }
43 | AuthManager.resetInstance();
44 | CredentialStore.resetInstance();
45 | vi.restoreAllMocks();
46 |
47 | // Remove temporary directory
48 | if (tmpDir && fs.existsSync(tmpDir)) {
49 | fs.rmSync(tmpDir, { recursive: true, force: true });
50 | }
51 | });
52 |
53 | it('should return expired credentials to enable refresh flows', () => {
54 | // Set up expired credentials with refresh token
55 | const expiredCredentials: AuthCredentials = {
56 | token: 'expired_access_token',
57 | refreshToken: 'valid_refresh_token',
58 | userId: 'test-user-id',
59 | email: '[email protected]',
60 | expiresAt: new Date(Date.now() - 1000).toISOString(), // Expired 1 second ago
61 | savedAt: new Date().toISOString()
62 | };
63 |
64 | credentialStore.saveCredentials(expiredCredentials);
65 |
66 | // Get credentials should return them even if expired
67 | // Refresh will be handled by explicit calls or client operations
68 | const credentials = authManager.getCredentials();
69 |
70 | expect(credentials).not.toBeNull();
71 | expect(credentials?.token).toBe('expired_access_token');
72 | expect(credentials?.refreshToken).toBe('valid_refresh_token');
73 | });
74 |
75 | it('should return valid credentials', () => {
76 | // Set up valid (non-expired) credentials
77 | const validCredentials: AuthCredentials = {
78 | token: 'valid_access_token',
79 | refreshToken: 'valid_refresh_token',
80 | userId: 'test-user-id',
81 | email: '[email protected]',
82 | expiresAt: new Date(Date.now() + 3600000).toISOString(), // Expires in 1 hour
83 | savedAt: new Date().toISOString()
84 | };
85 |
86 | credentialStore.saveCredentials(validCredentials);
87 |
88 | const credentials = authManager.getCredentials();
89 |
90 | expect(credentials?.token).toBe('valid_access_token');
91 | });
92 |
93 | it('should return expired credentials even without refresh token', () => {
94 | // Set up expired credentials WITHOUT refresh token
95 | // We still return them - it's up to the caller to handle
96 | const expiredCredentials: AuthCredentials = {
97 | token: 'expired_access_token',
98 | refreshToken: undefined,
99 | userId: 'test-user-id',
100 | email: '[email protected]',
101 | expiresAt: new Date(Date.now() - 1000).toISOString(), // Expired 1 second ago
102 | savedAt: new Date().toISOString()
103 | };
104 |
105 | credentialStore.saveCredentials(expiredCredentials);
106 |
107 | const credentials = authManager.getCredentials();
108 |
109 | // Returns credentials even if expired
110 | expect(credentials).not.toBeNull();
111 | expect(credentials?.token).toBe('expired_access_token');
112 | });
113 |
114 | it('should return null if no credentials exist', () => {
115 | const credentials = authManager.getCredentials();
116 | expect(credentials).toBeNull();
117 | });
118 |
119 | it('should return credentials regardless of refresh token validity', () => {
120 | // Set up expired credentials with refresh token
121 | const expiredCredentials: AuthCredentials = {
122 | token: 'expired_access_token',
123 | refreshToken: 'invalid_refresh_token',
124 | userId: 'test-user-id',
125 | email: '[email protected]',
126 | expiresAt: new Date(Date.now() - 1000).toISOString(),
127 | savedAt: new Date().toISOString()
128 | };
129 |
130 | credentialStore.saveCredentials(expiredCredentials);
131 |
132 | const credentials = authManager.getCredentials();
133 |
134 | // Returns credentials - refresh will be attempted by the client which will handle failure
135 | expect(credentials).not.toBeNull();
136 | expect(credentials?.token).toBe('expired_access_token');
137 | expect(credentials?.refreshToken).toBe('invalid_refresh_token');
138 | });
139 | });
140 |
```
--------------------------------------------------------------------------------
/src/ai-providers/google-vertex.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * google-vertex.js
3 | * AI provider implementation for Google Vertex AI models using Vercel AI SDK.
4 | */
5 |
6 | import { createVertex } from '@ai-sdk/google-vertex';
7 | import { BaseAIProvider } from './base-provider.js';
8 | import { resolveEnvVariable } from '../../scripts/modules/utils.js';
9 | import { log } from '../../scripts/modules/utils.js';
10 |
11 | // Vertex-specific error classes
12 | class VertexAuthError extends Error {
13 | constructor(message) {
14 | super(message);
15 | this.name = 'VertexAuthError';
16 | this.code = 'vertex_auth_error';
17 | }
18 | }
19 |
20 | class VertexConfigError extends Error {
21 | constructor(message) {
22 | super(message);
23 | this.name = 'VertexConfigError';
24 | this.code = 'vertex_config_error';
25 | }
26 | }
27 |
28 | class VertexApiError extends Error {
29 | constructor(message, statusCode) {
30 | super(message);
31 | this.name = 'VertexApiError';
32 | this.code = 'vertex_api_error';
33 | this.statusCode = statusCode;
34 | }
35 | }
36 |
37 | export class VertexAIProvider extends BaseAIProvider {
38 | constructor() {
39 | super();
40 | this.name = 'Google Vertex AI';
41 | }
42 |
43 | /**
44 | * Returns the required API key environment variable name for Google Vertex AI.
45 | * @returns {string} The environment variable name
46 | */
47 | getRequiredApiKeyName() {
48 | return 'GOOGLE_API_KEY';
49 | }
50 |
51 | /**
52 | * Validates Vertex AI-specific authentication parameters
53 | * @param {object} params - Parameters to validate
54 | * @throws {Error} If required parameters are missing
55 | */
56 | validateAuth(params) {
57 | const { apiKey, projectId, location, credentials } = params;
58 |
59 | // Check for API key OR service account credentials
60 | if (!apiKey && !credentials) {
61 | throw new VertexAuthError(
62 | 'Either Google API key (GOOGLE_API_KEY) or service account credentials (GOOGLE_APPLICATION_CREDENTIALS) is required for Vertex AI'
63 | );
64 | }
65 |
66 | // Project ID is required for Vertex AI
67 | if (!projectId) {
68 | throw new VertexConfigError(
69 | 'Google Cloud project ID is required for Vertex AI. Set VERTEX_PROJECT_ID environment variable.'
70 | );
71 | }
72 |
73 | // Location is required for Vertex AI
74 | if (!location) {
75 | throw new VertexConfigError(
76 | 'Google Cloud location is required for Vertex AI. Set VERTEX_LOCATION environment variable (e.g., "us-central1").'
77 | );
78 | }
79 | }
80 |
81 | /**
82 | * Creates and returns a Google Vertex AI client instance.
83 | * @param {object} params - Parameters for client initialization
84 | * @param {string} [params.apiKey] - Google API key
85 | * @param {string} params.projectId - Google Cloud project ID
86 | * @param {string} params.location - Google Cloud location (e.g., "us-central1")
87 | * @param {object} [params.credentials] - Service account credentials object
88 | * @param {string} [params.baseURL] - Optional custom API endpoint
89 | * @returns {Function} Google Vertex AI client function
90 | * @throws {Error} If required parameters are missing or initialization fails
91 | */
92 | getClient(params) {
93 | try {
94 | const { apiKey, projectId, location, credentials, baseURL } = params;
95 | const fetchImpl = this.createProxyFetch();
96 |
97 | // Configure auth options - either API key or service account
98 | const authOptions = {};
99 | if (apiKey) {
100 | authOptions.apiKey = apiKey;
101 | } else if (credentials) {
102 | authOptions.googleAuthOptions = credentials;
103 | }
104 |
105 | // Return Vertex AI client
106 | return createVertex({
107 | ...authOptions,
108 | projectId,
109 | location,
110 | ...(baseURL && { baseURL }),
111 | ...(fetchImpl && { fetch: fetchImpl })
112 | });
113 | } catch (error) {
114 | this.handleError('client initialization', error);
115 | }
116 | }
117 |
118 | /**
119 | * Handle errors from Vertex AI
120 | * @param {string} operation - Description of the operation that failed
121 | * @param {Error} error - The error object
122 | * @throws {Error} Rethrows the error with additional context
123 | */
124 | handleError(operation, error) {
125 | log('error', `Vertex AI ${operation} error:`, error);
126 |
127 | // Handle known error types
128 | if (
129 | error.name === 'VertexAuthError' ||
130 | error.name === 'VertexConfigError' ||
131 | error.name === 'VertexApiError'
132 | ) {
133 | throw error;
134 | }
135 |
136 | // Handle network/API errors
137 | if (error.response) {
138 | const statusCode = error.response.status;
139 | const errorMessage = error.response.data?.error?.message || error.message;
140 |
141 | // Categorize by status code
142 | if (statusCode === 401 || statusCode === 403) {
143 | throw new VertexAuthError(`Authentication failed: ${errorMessage}`);
144 | } else if (statusCode === 400) {
145 | throw new VertexConfigError(`Invalid request: ${errorMessage}`);
146 | } else {
147 | throw new VertexApiError(
148 | `API error (${statusCode}): ${errorMessage}`,
149 | statusCode
150 | );
151 | }
152 | }
153 |
154 | // Generic error handling
155 | throw new Error(`Vertex AI ${operation} failed: ${error.message}`);
156 | }
157 | }
158 |
```
--------------------------------------------------------------------------------
/apps/cli/src/commands/autopilot/start.command.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Start Command - Initialize and start TDD workflow
3 | */
4 |
5 | import { type WorkflowContext, createTmCore } from '@tm/core';
6 | import { Command } from 'commander';
7 | import {
8 | AutopilotBaseOptions,
9 | OutputFormatter,
10 | createGitAdapter,
11 | createOrchestrator,
12 | hasWorkflowState,
13 | parseSubtasks,
14 | validateTaskId
15 | } from './shared.js';
16 | import { getProjectRoot } from '../../utils/project-root.js';
17 |
18 | interface StartOptions extends AutopilotBaseOptions {
19 | force?: boolean;
20 | maxAttempts?: string;
21 | }
22 |
23 | /**
24 | * Start Command - Initialize new TDD workflow
25 | */
26 | export class StartCommand extends Command {
27 | constructor() {
28 | super('start');
29 |
30 | this.description('Initialize and start a new TDD workflow for a task')
31 | .argument('<taskId>', 'Task ID to start workflow for')
32 | .option('-f, --force', 'Force start even if workflow state exists')
33 | .option('--max-attempts <number>', 'Maximum attempts per subtask', '3')
34 | .action(async (taskId: string, options: StartOptions) => {
35 | await this.execute(taskId, options);
36 | });
37 | }
38 |
39 | private async execute(taskId: string, options: StartOptions): Promise<void> {
40 | // Inherit parent options
41 | const parentOpts = this.parent?.opts() as AutopilotBaseOptions;
42 | const mergedOptions: StartOptions = {
43 | ...parentOpts,
44 | ...options,
45 | projectRoot: getProjectRoot(
46 | options.projectRoot || parentOpts?.projectRoot
47 | )
48 | };
49 |
50 | const formatter = new OutputFormatter(mergedOptions.json || false);
51 |
52 | try {
53 | // Validate task ID
54 | if (!validateTaskId(taskId)) {
55 | formatter.error('Invalid task ID format', {
56 | taskId,
57 | expected: 'Format: number or number.number (e.g., "1" or "1.2")'
58 | });
59 | process.exit(1);
60 | }
61 |
62 | // Check for existing workflow state
63 | const hasState = await hasWorkflowState(mergedOptions.projectRoot!);
64 | if (hasState && !mergedOptions.force) {
65 | formatter.error(
66 | 'Workflow state already exists. Use --force to overwrite or resume with "autopilot resume"'
67 | );
68 | process.exit(1);
69 | }
70 |
71 | // Initialize Task Master Core
72 | const tmCore = await createTmCore({
73 | projectPath: mergedOptions.projectRoot!
74 | });
75 |
76 | // Get current tag from ConfigManager
77 | const currentTag = tmCore.config.getActiveTag();
78 |
79 | // Load task
80 | formatter.info(`Loading task ${taskId}...`);
81 | const { task } = await tmCore.tasks.get(taskId);
82 |
83 | if (!task) {
84 | formatter.error('Task not found', { taskId });
85 | process.exit(1);
86 | }
87 |
88 | // Validate task has subtasks
89 | if (!task.subtasks || task.subtasks.length === 0) {
90 | formatter.error('Task has no subtasks. Expand task first.', {
91 | taskId,
92 | suggestion: `Run: task-master expand --id=${taskId}`
93 | });
94 | process.exit(1);
95 | }
96 |
97 | // Initialize Git adapter
98 | const gitAdapter = createGitAdapter(mergedOptions.projectRoot!);
99 | await gitAdapter.ensureGitRepository();
100 | await gitAdapter.ensureCleanWorkingTree();
101 |
102 | // Parse subtasks
103 | const maxAttempts = parseInt(mergedOptions.maxAttempts || '3', 10);
104 | const subtasks = parseSubtasks(task, maxAttempts);
105 |
106 | // Create workflow context
107 | const context: WorkflowContext = {
108 | taskId: task.id,
109 | subtasks,
110 | currentSubtaskIndex: 0,
111 | errors: [],
112 | metadata: {
113 | startedAt: new Date().toISOString(),
114 | tags: task.tags || []
115 | }
116 | };
117 |
118 | // Create orchestrator with persistence
119 | const orchestrator = createOrchestrator(
120 | context,
121 | mergedOptions.projectRoot!
122 | );
123 |
124 | // Complete PREFLIGHT phase
125 | orchestrator.transition({ type: 'PREFLIGHT_COMPLETE' });
126 |
127 | // Generate descriptive branch name
128 | const sanitizedTitle = task.title
129 | .toLowerCase()
130 | .replace(/[^a-z0-9]+/g, '-')
131 | .replace(/^-+|-+$/g, '')
132 | .substring(0, 50);
133 | const formattedTaskId = taskId.replace(/\./g, '-');
134 | const tagPrefix = currentTag ? `${currentTag}/` : '';
135 | const branchName = `${tagPrefix}task-${formattedTaskId}-${sanitizedTitle}`;
136 |
137 | // Create and checkout branch
138 | formatter.info(`Creating branch: ${branchName}`);
139 | await gitAdapter.createAndCheckoutBranch(branchName);
140 |
141 | // Transition to SUBTASK_LOOP
142 | orchestrator.transition({
143 | type: 'BRANCH_CREATED',
144 | branchName
145 | });
146 |
147 | // Output success
148 | formatter.success('TDD workflow started', {
149 | taskId: task.id,
150 | title: task.title,
151 | phase: orchestrator.getCurrentPhase(),
152 | tddPhase: orchestrator.getCurrentTDDPhase(),
153 | branchName,
154 | subtasks: subtasks.length,
155 | currentSubtask: subtasks[0]?.title
156 | });
157 |
158 | // Clean up
159 | } catch (error) {
160 | formatter.error((error as Error).message);
161 | if (mergedOptions.verbose) {
162 | console.error((error as Error).stack);
163 | }
164 | process.exit(1);
165 | }
166 | }
167 | }
168 |
```
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Task Master
5 | * Copyright (c) 2025 Eyal Toledano, Ralph Khreish
6 | *
7 | * This software is licensed under the MIT License with Commons Clause.
8 | * You may use this software for any purpose, including commercial applications,
9 | * and modify and redistribute it freely, subject to the following restrictions:
10 | *
11 | * 1. You may not sell this software or offer it as a service.
12 | * 2. The origin of this software must not be misrepresented.
13 | * 3. Altered source versions must be plainly marked as such.
14 | *
15 | * For the full license text, see the LICENSE file in the root directory.
16 | */
17 |
18 | /**
19 | * Claude Task Master
20 | * A task management system for AI-driven development with Claude
21 | */
22 |
23 | // This file serves as the main entry point for the package
24 | // The primary functionality is provided through the CLI commands
25 |
26 | import { fileURLToPath } from 'url';
27 | import { dirname, resolve } from 'path';
28 | import { createRequire } from 'module';
29 | import { spawn } from 'child_process';
30 | import { Command } from 'commander';
31 |
32 | const __filename = fileURLToPath(import.meta.url);
33 | const __dirname = dirname(__filename);
34 | const require = createRequire(import.meta.url);
35 |
36 | // Get package information
37 | const packageJson = require('./package.json');
38 |
39 | // Export the path to the dev.js script for programmatic usage
40 | export const devScriptPath = resolve(__dirname, './scripts/dev.js');
41 |
42 | // Export a function to initialize a new project programmatically
43 | export const initProject = async (options = {}) => {
44 | const init = await import('./scripts/init.js');
45 | return init.initializeProject(options);
46 | };
47 |
48 | // Export a function to run init as a CLI command
49 | export const runInitCLI = async (options = {}) => {
50 | try {
51 | const init = await import('./scripts/init.js');
52 | const result = await init.initializeProject(options);
53 | return result;
54 | } catch (error) {
55 | console.error('Initialization failed:', error.message);
56 | if (process.env.DEBUG === 'true') {
57 | console.error('Debug stack trace:', error.stack);
58 | }
59 | throw error; // Re-throw to be handled by the command handler
60 | }
61 | };
62 |
63 | // Export version information
64 | export const version = packageJson.version;
65 |
66 | // CLI implementation
67 | if (import.meta.url === `file://${process.argv[1]}`) {
68 | const program = new Command();
69 |
70 | program
71 | .name('task-master')
72 | .description('Claude Task Master CLI')
73 | .version(version);
74 |
75 | program
76 | .command('init')
77 | .description('Initialize a new project')
78 | .option('-y, --yes', 'Skip prompts and use default values')
79 | .option('-n, --name <n>', 'Project name')
80 | .option('-d, --description <description>', 'Project description')
81 | .option('-v, --version <version>', 'Project version', '0.1.0')
82 | .option('-a, --author <author>', 'Author name')
83 | .option('--skip-install', 'Skip installing dependencies')
84 | .option('--dry-run', 'Show what would be done without making changes')
85 | .option('--aliases', 'Add shell aliases (tm, taskmaster)')
86 | .option('--no-aliases', 'Skip shell aliases (tm, taskmaster)')
87 | .option('--git', 'Initialize Git repository')
88 | .option('--no-git', 'Skip Git repository initialization')
89 | .option('--git-tasks', 'Store tasks in Git')
90 | .option('--no-git-tasks', 'No Git storage of tasks')
91 | .action(async (cmdOptions) => {
92 | try {
93 | await runInitCLI(cmdOptions);
94 | } catch (err) {
95 | console.error('Init failed:', err.message);
96 | process.exit(1);
97 | }
98 | });
99 |
100 | program
101 | .command('dev')
102 | .description('Run the dev.js script')
103 | .allowUnknownOption(true)
104 | .action(() => {
105 | const args = process.argv.slice(process.argv.indexOf('dev') + 1);
106 | const child = spawn('node', [devScriptPath, ...args], {
107 | stdio: 'inherit',
108 | cwd: process.cwd()
109 | });
110 |
111 | child.on('close', (code) => {
112 | process.exit(code);
113 | });
114 | });
115 |
116 | // Add shortcuts for common dev.js commands
117 | program
118 | .command('list')
119 | .description('List all tasks')
120 | .action(() => {
121 | const child = spawn('node', [devScriptPath, 'list'], {
122 | stdio: 'inherit',
123 | cwd: process.cwd()
124 | });
125 |
126 | child.on('close', (code) => {
127 | process.exit(code);
128 | });
129 | });
130 |
131 | program
132 | .command('next')
133 | .description('Show the next task to work on')
134 | .action(() => {
135 | const child = spawn('node', [devScriptPath, 'next'], {
136 | stdio: 'inherit',
137 | cwd: process.cwd()
138 | });
139 |
140 | child.on('close', (code) => {
141 | process.exit(code);
142 | });
143 | });
144 |
145 | program
146 | .command('generate')
147 | .description('Generate task files')
148 | .action(() => {
149 | const child = spawn('node', [devScriptPath, 'generate'], {
150 | stdio: 'inherit',
151 | cwd: process.cwd()
152 | });
153 |
154 | child.on('close', (code) => {
155 | process.exit(code);
156 | });
157 | });
158 |
159 | program.parse(process.argv);
160 | }
161 |
```
--------------------------------------------------------------------------------
/apps/mcp/src/tools/tasks/get-tasks.tool.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview get-tasks MCP tool
3 | * Get all tasks from Task Master with optional filtering
4 | */
5 |
6 | import { z } from 'zod';
7 | import {
8 | handleApiResult,
9 | withNormalizedProjectRoot
10 | } from '../../shared/utils.js';
11 | import type { MCPContext } from '../../shared/types.js';
12 | import { createTmCore, type TaskStatus, type Task } from '@tm/core';
13 | import type { FastMCP } from 'fastmcp';
14 |
15 | const GetTasksSchema = z.object({
16 | projectRoot: z
17 | .string()
18 | .describe('The directory of the project. Must be an absolute path.'),
19 | status: z
20 | .string()
21 | .optional()
22 | .describe(
23 | "Filter tasks by status (e.g., 'pending', 'done') or multiple statuses separated by commas (e.g., 'blocked,deferred')"
24 | ),
25 | withSubtasks: z
26 | .boolean()
27 | .optional()
28 | .describe('Include subtasks nested within their parent tasks in the response'),
29 | tag: z.string().optional().describe('Tag context to operate on')
30 | });
31 |
32 | type GetTasksArgs = z.infer<typeof GetTasksSchema>;
33 |
34 | /**
35 | * Register the get_tasks tool with the MCP server
36 | */
37 | export function registerGetTasksTool(server: FastMCP) {
38 | server.addTool({
39 | name: 'get_tasks',
40 | description:
41 | 'Get all tasks from Task Master, optionally filtering by status and including subtasks.',
42 | parameters: GetTasksSchema,
43 | execute: withNormalizedProjectRoot(
44 | async (args: GetTasksArgs, context: MCPContext) => {
45 | const { projectRoot, status, withSubtasks, tag } = args;
46 |
47 | try {
48 | context.log.info(
49 | `Getting tasks from ${projectRoot}${status ? ` with status filter: ${status}` : ''}${tag ? ` for tag: ${tag}` : ''}`
50 | );
51 |
52 | // Create tm-core with logging callback
53 | const tmCore = await createTmCore({
54 | projectPath: projectRoot,
55 | loggerConfig: {
56 | mcpMode: true,
57 | logCallback: context.log
58 | }
59 | });
60 |
61 | // Build filter
62 | const filter =
63 | status && status !== 'all'
64 | ? {
65 | status: status
66 | .split(',')
67 | .map((s: string) => s.trim() as TaskStatus)
68 | }
69 | : undefined;
70 |
71 | // Call tm-core tasks.list()
72 | const result = await tmCore.tasks.list({
73 | tag,
74 | filter,
75 | includeSubtasks: withSubtasks
76 | });
77 |
78 | context.log.info(
79 | `Retrieved ${result.tasks?.length || 0} tasks (${result.filtered} filtered, ${result.total} total)`
80 | );
81 |
82 | // Calculate stats using reduce for cleaner code
83 | const totalTasks = result.total;
84 | const taskCounts = result.tasks.reduce(
85 | (acc, task) => {
86 | acc[task.status] = (acc[task.status] || 0) + 1;
87 | return acc;
88 | },
89 | {} as Record<string, number>
90 | );
91 |
92 | const completionPercentage =
93 | totalTasks > 0 ? ((taskCounts.done || 0) / totalTasks) * 100 : 0;
94 |
95 | // Count subtasks using reduce
96 | const subtaskCounts = result.tasks.reduce(
97 | (acc, task) => {
98 | task.subtasks?.forEach((st) => {
99 | acc.total++;
100 | acc[st.status] = (acc[st.status] || 0) + 1;
101 | });
102 | return acc;
103 | },
104 | { total: 0 } as Record<string, number>
105 | );
106 |
107 | const subtaskCompletionPercentage =
108 | subtaskCounts.total > 0
109 | ? ((subtaskCounts.done || 0) / subtaskCounts.total) * 100
110 | : 0;
111 |
112 | return handleApiResult({
113 | result: {
114 | success: true,
115 | data: {
116 | tasks: result.tasks as Task[],
117 | filter: status || 'all',
118 | stats: {
119 | total: totalTasks,
120 | completed: taskCounts.done || 0,
121 | inProgress: taskCounts['in-progress'] || 0,
122 | pending: taskCounts.pending || 0,
123 | blocked: taskCounts.blocked || 0,
124 | deferred: taskCounts.deferred || 0,
125 | cancelled: taskCounts.cancelled || 0,
126 | review: taskCounts.review || 0,
127 | completionPercentage,
128 | subtasks: {
129 | total: subtaskCounts.total,
130 | completed: subtaskCounts.done || 0,
131 | inProgress: subtaskCounts['in-progress'] || 0,
132 | pending: subtaskCounts.pending || 0,
133 | blocked: subtaskCounts.blocked || 0,
134 | deferred: subtaskCounts.deferred || 0,
135 | cancelled: subtaskCounts.cancelled || 0,
136 | completionPercentage: subtaskCompletionPercentage
137 | }
138 | }
139 | }
140 | },
141 | log: context.log,
142 | projectRoot,
143 | tag: result.tag
144 | });
145 | } catch (error: any) {
146 | context.log.error(`Error in get-tasks: ${error.message}`);
147 | if (error.stack) {
148 | context.log.debug(error.stack);
149 | }
150 | return handleApiResult({
151 | result: {
152 | success: false,
153 | error: {
154 | message: `Failed to get tasks: ${error.message}`
155 | }
156 | },
157 | log: context.log,
158 | projectRoot
159 | });
160 | }
161 | }
162 | )
163 | });
164 | }
165 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/storage/adapters/activity-logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Activity.jsonl append-only logging system for workflow tracking.
3 | * Uses newline-delimited JSON (JSONL) format for structured event logging.
4 | *
5 | * @module activity-logger
6 | */
7 |
8 | import path from 'path';
9 | import fs from 'fs-extra';
10 |
11 | /**
12 | * Activity log entry structure
13 | */
14 | export interface ActivityEvent {
15 | timestamp: string;
16 | type: string;
17 | [key: string]: any;
18 | }
19 |
20 | /**
21 | * Filter criteria for activity log queries
22 | */
23 | export interface ActivityFilter {
24 | type?: string;
25 | timestampFrom?: string;
26 | timestampTo?: string;
27 | predicate?: (event: ActivityEvent) => boolean;
28 | }
29 |
30 | /**
31 | * Appends an activity event to the log file.
32 | * Uses atomic append operations to ensure data integrity.
33 | *
34 | * @param {string} activityPath - Path to the activity.jsonl file
35 | * @param {Omit<ActivityEvent, 'timestamp'>} event - Event data to log (timestamp added automatically)
36 | * @returns {Promise<void>}
37 | *
38 | * @example
39 | * await logActivity('/path/to/activity.jsonl', {
40 | * type: 'phase-start',
41 | * phase: 'red'
42 | * });
43 | */
44 | export async function logActivity(
45 | activityPath: string,
46 | event: Omit<ActivityEvent, 'timestamp'>
47 | ): Promise<void> {
48 | // Add timestamp to event
49 | const logEntry = {
50 | ...event,
51 | timestamp: new Date().toISOString()
52 | } as ActivityEvent;
53 |
54 | // Ensure directory exists
55 | await fs.ensureDir(path.dirname(activityPath));
56 |
57 | // Convert to JSONL format (single line with newline)
58 | const line = JSON.stringify(logEntry) + '\n';
59 |
60 | // Append to file atomically
61 | // Using 'a' flag ensures atomic append on most systems
62 | await fs.appendFile(activityPath, line, 'utf-8');
63 | }
64 |
65 | /**
66 | * Reads and parses all events from an activity log file.
67 | * Returns events in chronological order.
68 | *
69 | * @param {string} activityPath - Path to the activity.jsonl file
70 | * @returns {Promise<ActivityEvent[]>} Array of activity events
71 | * @throws {Error} If file contains invalid JSON
72 | *
73 | * @example
74 | * const events = await readActivityLog('/path/to/activity.jsonl');
75 | * console.log(`Found ${events.length} events`);
76 | */
77 | export async function readActivityLog(
78 | activityPath: string
79 | ): Promise<ActivityEvent[]> {
80 | // Return empty array if file doesn't exist
81 | if (!(await fs.pathExists(activityPath))) {
82 | return [];
83 | }
84 |
85 | // Read file content
86 | const content = await fs.readFile(activityPath, 'utf-8');
87 |
88 | // Parse JSONL (newline-delimited JSON)
89 | const lines = content.trim().split('\n');
90 | const events: ActivityEvent[] = [];
91 |
92 | for (let i = 0; i < lines.length; i++) {
93 | const line = lines[i].trim();
94 |
95 | // Skip empty lines
96 | if (!line) {
97 | continue;
98 | }
99 |
100 | // Parse JSON
101 | try {
102 | const event = JSON.parse(line);
103 | events.push(event);
104 | } catch (error) {
105 | const errorMessage =
106 | error instanceof Error ? error.message : String(error);
107 | throw new Error(`Invalid JSON at line ${i + 1}: ${errorMessage}`);
108 | }
109 | }
110 |
111 | return events;
112 | }
113 |
114 | /**
115 | * Filters activity log events based on criteria.
116 | * Supports filtering by event type, timestamp range, and custom predicates.
117 | *
118 | * @param {string} activityPath - Path to the activity.jsonl file
119 | * @param {ActivityFilter} filter - Filter criteria
120 | * @returns {Promise<ActivityEvent[]>} Filtered array of events
121 | *
122 | * @example
123 | * // Filter by event type
124 | * const phaseEvents = await filterActivityLog('/path/to/activity.jsonl', {
125 | * type: 'phase-start'
126 | * });
127 | *
128 | * // Filter by timestamp range
129 | * const recentEvents = await filterActivityLog('/path/to/activity.jsonl', {
130 | * timestampFrom: '2024-01-15T10:00:00.000Z'
131 | * });
132 | *
133 | * // Filter with custom predicate
134 | * const failedTests = await filterActivityLog('/path/to/activity.jsonl', {
135 | * predicate: (event) => event.type === 'test-run' && event.result === 'fail'
136 | * });
137 | */
138 | export async function filterActivityLog(
139 | activityPath: string,
140 | filter: ActivityFilter & Record<string, any>
141 | ): Promise<ActivityEvent[]> {
142 | const events = await readActivityLog(activityPath);
143 |
144 | return events.filter((event) => {
145 | // Filter by type
146 | if (filter.type && event.type !== filter.type) {
147 | return false;
148 | }
149 |
150 | // Filter by timestamp range
151 | if (filter.timestampFrom && event.timestamp < filter.timestampFrom) {
152 | return false;
153 | }
154 |
155 | if (filter.timestampTo && event.timestamp > filter.timestampTo) {
156 | return false;
157 | }
158 |
159 | // Filter by custom predicate
160 | if (filter.predicate && !filter.predicate(event)) {
161 | return false;
162 | }
163 |
164 | // Filter by other fields (exact match)
165 | for (const [key, value] of Object.entries(filter)) {
166 | if (
167 | key === 'type' ||
168 | key === 'timestampFrom' ||
169 | key === 'timestampTo' ||
170 | key === 'predicate'
171 | ) {
172 | continue;
173 | }
174 |
175 | if (event[key] !== value) {
176 | return false;
177 | }
178 | }
179 |
180 | return true;
181 | });
182 | }
183 |
```
--------------------------------------------------------------------------------
/scripts/modules/task-manager/find-next-task.js:
--------------------------------------------------------------------------------
```javascript
1 | import { log } from '../utils.js';
2 | import { addComplexityToTask } from '../utils.js';
3 |
4 | /**
5 | * Return the next work item:
6 | * • Prefer an eligible SUBTASK that belongs to any parent task
7 | * whose own status is `in-progress`.
8 | * • If no such subtask exists, fall back to the best top-level task
9 | * (previous behaviour).
10 | *
11 | * The function still exports the same name (`findNextTask`) so callers
12 | * don't need to change. It now always returns an object with
13 | * ─ id → number (task) or "parentId.subId" (subtask)
14 | * ─ title → string
15 | * ─ status → string
16 | * ─ priority → string ("high" | "medium" | "low")
17 | * ─ dependencies → array (all IDs expressed in the same dotted form)
18 | * ─ parentId → number (present only when it's a subtask)
19 | *
20 | * @param {Object[]} tasks – full array of top-level tasks, each may contain .subtasks[]
21 | * @param {Object} [complexityReport=null] - Optional complexity report object
22 | * @returns {Object|null} – next work item or null if nothing is eligible
23 | */
24 | function findNextTask(tasks, complexityReport = null) {
25 | // ---------- helpers ----------------------------------------------------
26 | const priorityValues = { high: 3, medium: 2, low: 1 };
27 |
28 | const toFullSubId = (parentId, maybeDotId) => {
29 | // "12.3" -> "12.3"
30 | // 4 -> "12.4" (numeric / short form)
31 | if (typeof maybeDotId === 'string' && maybeDotId.includes('.')) {
32 | return maybeDotId;
33 | }
34 | return `${parentId}.${maybeDotId}`;
35 | };
36 |
37 | // ---------- build completed-ID set (tasks *and* subtasks) --------------
38 | const completedIds = new Set();
39 | tasks.forEach((t) => {
40 | if (t.status === 'done' || t.status === 'completed') {
41 | completedIds.add(String(t.id));
42 | }
43 | if (Array.isArray(t.subtasks)) {
44 | t.subtasks.forEach((st) => {
45 | if (st.status === 'done' || st.status === 'completed') {
46 | completedIds.add(`${t.id}.${st.id}`);
47 | }
48 | });
49 | }
50 | });
51 |
52 | // ---------- 1) look for eligible subtasks ------------------------------
53 | const candidateSubtasks = [];
54 |
55 | tasks
56 | .filter((t) => t.status === 'in-progress' && Array.isArray(t.subtasks))
57 | .forEach((parent) => {
58 | parent.subtasks.forEach((st) => {
59 | const stStatus = (st.status || 'pending').toLowerCase();
60 | if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
61 |
62 | const fullDeps =
63 | st.dependencies?.map((d) => toFullSubId(parent.id, d)) ?? [];
64 |
65 | const depsSatisfied =
66 | fullDeps.length === 0 ||
67 | fullDeps.every((depId) => completedIds.has(String(depId)));
68 |
69 | if (depsSatisfied) {
70 | candidateSubtasks.push({
71 | id: `${parent.id}.${st.id}`,
72 | title: st.title || `Subtask ${st.id}`,
73 | status: st.status || 'pending',
74 | priority: st.priority || parent.priority || 'medium',
75 | dependencies: fullDeps,
76 | parentId: parent.id
77 | });
78 | }
79 | });
80 | });
81 |
82 | if (candidateSubtasks.length > 0) {
83 | // sort by priority → dep-count → parent-id → sub-id
84 | candidateSubtasks.sort((a, b) => {
85 | const pa = priorityValues[a.priority] ?? 2;
86 | const pb = priorityValues[b.priority] ?? 2;
87 | if (pb !== pa) return pb - pa;
88 |
89 | if (a.dependencies.length !== b.dependencies.length)
90 | return a.dependencies.length - b.dependencies.length;
91 |
92 | // compare parent then sub-id numerically
93 | const [aPar, aSub] = a.id.split('.').map(Number);
94 | const [bPar, bSub] = b.id.split('.').map(Number);
95 | if (aPar !== bPar) return aPar - bPar;
96 | return aSub - bSub;
97 | });
98 | const nextTask = candidateSubtasks[0];
99 |
100 | // Add complexity to the task before returning
101 | if (nextTask && complexityReport) {
102 | addComplexityToTask(nextTask, complexityReport);
103 | }
104 |
105 | return nextTask;
106 | }
107 |
108 | // ---------- 2) fall back to top-level tasks (original logic) ------------
109 | const eligibleTasks = tasks.filter((task) => {
110 | const status = (task.status || 'pending').toLowerCase();
111 | if (status !== 'pending' && status !== 'in-progress') return false;
112 | const deps = task.dependencies ?? [];
113 | return deps.every((depId) => completedIds.has(String(depId)));
114 | });
115 |
116 | if (eligibleTasks.length === 0) return null;
117 |
118 | const nextTask = eligibleTasks.sort((a, b) => {
119 | const pa = priorityValues[a.priority || 'medium'] ?? 2;
120 | const pb = priorityValues[b.priority || 'medium'] ?? 2;
121 | if (pb !== pa) return pb - pa;
122 |
123 | const da = (a.dependencies ?? []).length;
124 | const db = (b.dependencies ?? []).length;
125 | if (da !== db) return da - db;
126 |
127 | return a.id - b.id;
128 | })[0];
129 |
130 | // Add complexity to the task before returning
131 | if (nextTask && complexityReport) {
132 | addComplexityToTask(nextTask, complexityReport);
133 | }
134 |
135 | return nextTask;
136 | }
137 |
138 | export default findNextTask;
139 |
```
--------------------------------------------------------------------------------
/tests/unit/ai-providers/openai.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Tests for OpenAI Provider
3 | *
4 | * This test suite covers:
5 | * 1. Validation of maxTokens parameter
6 | * 2. Client creation and configuration
7 | * 3. Model handling
8 | */
9 |
10 | import { jest } from '@jest/globals';
11 |
12 | // Mock the utils module to prevent logging during tests
13 | jest.mock('../../../scripts/modules/utils.js', () => ({
14 | log: jest.fn()
15 | }));
16 |
17 | // Import the provider
18 | import { OpenAIProvider } from '../../../src/ai-providers/openai.js';
19 |
20 | describe('OpenAIProvider', () => {
21 | let provider;
22 |
23 | beforeEach(() => {
24 | provider = new OpenAIProvider();
25 | jest.clearAllMocks();
26 | });
27 |
28 | describe('validateOptionalParams', () => {
29 | it('should accept valid maxTokens values', () => {
30 | expect(() =>
31 | provider.validateOptionalParams({ maxTokens: 1000 })
32 | ).not.toThrow();
33 | expect(() =>
34 | provider.validateOptionalParams({ maxTokens: 1 })
35 | ).not.toThrow();
36 | expect(() =>
37 | provider.validateOptionalParams({ maxTokens: '1000' })
38 | ).not.toThrow();
39 | });
40 |
41 | it('should reject invalid maxTokens values', () => {
42 | expect(() => provider.validateOptionalParams({ maxTokens: 0 })).toThrow(
43 | Error
44 | );
45 | expect(() => provider.validateOptionalParams({ maxTokens: -1 })).toThrow(
46 | Error
47 | );
48 | expect(() => provider.validateOptionalParams({ maxTokens: NaN })).toThrow(
49 | Error
50 | );
51 | expect(() =>
52 | provider.validateOptionalParams({ maxTokens: Infinity })
53 | ).toThrow(Error);
54 | expect(() =>
55 | provider.validateOptionalParams({ maxTokens: 'invalid' })
56 | ).toThrow(Error);
57 | });
58 |
59 | it('should accept valid temperature values', () => {
60 | expect(() =>
61 | provider.validateOptionalParams({ temperature: 0 })
62 | ).not.toThrow();
63 | expect(() =>
64 | provider.validateOptionalParams({ temperature: 0.5 })
65 | ).not.toThrow();
66 | expect(() =>
67 | provider.validateOptionalParams({ temperature: 1 })
68 | ).not.toThrow();
69 | });
70 |
71 | it('should reject invalid temperature values', () => {
72 | expect(() =>
73 | provider.validateOptionalParams({ temperature: -0.1 })
74 | ).toThrow(Error);
75 | expect(() =>
76 | provider.validateOptionalParams({ temperature: 1.1 })
77 | ).toThrow(Error);
78 | });
79 | });
80 |
81 | describe('getRequiredApiKeyName', () => {
82 | it('should return OPENAI_API_KEY', () => {
83 | expect(provider.getRequiredApiKeyName()).toBe('OPENAI_API_KEY');
84 | });
85 | });
86 |
87 | describe('getClient', () => {
88 | it('should create client even without API key (validation deferred to SDK)', () => {
89 | // getClient() no longer validates API key - validation is deferred to SDK initialization
90 | const client = provider.getClient({});
91 | expect(typeof client).toBe('function');
92 | });
93 |
94 | it('should create client with apiKey only', () => {
95 | const params = {
96 | apiKey: 'sk-test-123'
97 | };
98 |
99 | // The getClient method should return a function
100 | const client = provider.getClient(params);
101 | expect(typeof client).toBe('function');
102 |
103 | // The client function should be callable and return a model object
104 | const model = client('gpt-4');
105 | expect(model).toBeDefined();
106 | expect(model.modelId).toBe('gpt-4');
107 | });
108 |
109 | it('should create client with apiKey and baseURL', () => {
110 | const params = {
111 | apiKey: 'sk-test-456',
112 | baseURL: 'https://api.openai.example'
113 | };
114 |
115 | // Should not throw when baseURL is provided
116 | const client = provider.getClient(params);
117 | expect(typeof client).toBe('function');
118 |
119 | // The client function should be callable and return a model object
120 | const model = client('gpt-5');
121 | expect(model).toBeDefined();
122 | expect(model.modelId).toBe('gpt-5');
123 | });
124 |
125 | it('should return the same client instance for the same parameters', () => {
126 | const params = {
127 | apiKey: 'sk-test-789'
128 | };
129 |
130 | // Multiple calls with same params should work
131 | const client1 = provider.getClient(params);
132 | const client2 = provider.getClient(params);
133 |
134 | expect(typeof client1).toBe('function');
135 | expect(typeof client2).toBe('function');
136 |
137 | // Both clients should be able to create models
138 | const model1 = client1('gpt-4');
139 | const model2 = client2('gpt-4');
140 | expect(model1.modelId).toBe('gpt-4');
141 | expect(model2.modelId).toBe('gpt-4');
142 | });
143 |
144 | it('should handle different model IDs correctly', () => {
145 | const client = provider.getClient({ apiKey: 'sk-test-models' });
146 |
147 | // Test with different models
148 | const gpt4 = client('gpt-4');
149 | expect(gpt4.modelId).toBe('gpt-4');
150 |
151 | const gpt5 = client('gpt-5');
152 | expect(gpt5.modelId).toBe('gpt-5');
153 |
154 | const gpt35 = client('gpt-3.5-turbo');
155 | expect(gpt35.modelId).toBe('gpt-3.5-turbo');
156 | });
157 | });
158 |
159 | describe('name property', () => {
160 | it('should have OpenAI as the provider name', () => {
161 | expect(provider.name).toBe('OpenAI');
162 | });
163 | });
164 | });
165 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/services/config-loader.service.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Configuration Loader Service
3 | * Responsible for loading configuration from various file sources
4 | */
5 |
6 | import fs from 'node:fs/promises';
7 | import path from 'node:path';
8 | import {
9 | ERROR_CODES,
10 | TaskMasterError
11 | } from '../../../common/errors/task-master-error.js';
12 | import type { PartialConfiguration } from '../../../common/interfaces/configuration.interface.js';
13 | import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
14 |
15 | /**
16 | * ConfigLoader handles loading configuration from files
17 | * Single responsibility: File-based configuration loading
18 | */
19 | export class ConfigLoader {
20 | private localConfigPath: string;
21 | private globalConfigPath: string;
22 |
23 | constructor(projectRoot: string) {
24 | this.localConfigPath = path.join(projectRoot, '.taskmaster', 'config.json');
25 | this.globalConfigPath = path.join(
26 | process.env.HOME || '',
27 | '.taskmaster',
28 | 'config.json'
29 | );
30 | }
31 |
32 | /**
33 | * Get default configuration values
34 | */
35 | getDefaultConfig(): PartialConfiguration {
36 | return {
37 | models: {
38 | main: DEFAULT_CONFIG_VALUES.MODELS.MAIN,
39 | fallback: DEFAULT_CONFIG_VALUES.MODELS.FALLBACK
40 | },
41 | workflow: {
42 | enableAutopilot: DEFAULT_CONFIG_VALUES.WORKFLOW.ENABLE_AUTOPILOT,
43 | maxPhaseAttempts: DEFAULT_CONFIG_VALUES.WORKFLOW.MAX_PHASE_ATTEMPTS,
44 | branchPattern: DEFAULT_CONFIG_VALUES.WORKFLOW.BRANCH_PATTERN,
45 | requireCleanWorkingTree:
46 | DEFAULT_CONFIG_VALUES.WORKFLOW.REQUIRE_CLEAN_WORKING_TREE,
47 | autoStageChanges: DEFAULT_CONFIG_VALUES.WORKFLOW.AUTO_STAGE_CHANGES,
48 | includeCoAuthor: DEFAULT_CONFIG_VALUES.WORKFLOW.INCLUDE_CO_AUTHOR,
49 | coAuthorName: DEFAULT_CONFIG_VALUES.WORKFLOW.CO_AUTHOR_NAME,
50 | coAuthorEmail: DEFAULT_CONFIG_VALUES.WORKFLOW.CO_AUTHOR_EMAIL,
51 | testThresholds: {
52 | minTests: DEFAULT_CONFIG_VALUES.WORKFLOW.MIN_TESTS,
53 | maxFailuresInGreen:
54 | DEFAULT_CONFIG_VALUES.WORKFLOW.MAX_FAILURES_IN_GREEN
55 | },
56 | commitMessageTemplate:
57 | DEFAULT_CONFIG_VALUES.WORKFLOW.COMMIT_MESSAGE_TEMPLATE,
58 | allowedCommitTypes: [
59 | ...DEFAULT_CONFIG_VALUES.WORKFLOW.ALLOWED_COMMIT_TYPES
60 | ],
61 | defaultCommitType: DEFAULT_CONFIG_VALUES.WORKFLOW.DEFAULT_COMMIT_TYPE,
62 | operationTimeout: DEFAULT_CONFIG_VALUES.WORKFLOW.OPERATION_TIMEOUT,
63 | enableActivityLogging:
64 | DEFAULT_CONFIG_VALUES.WORKFLOW.ENABLE_ACTIVITY_LOGGING,
65 | activityLogPath: DEFAULT_CONFIG_VALUES.WORKFLOW.ACTIVITY_LOG_PATH,
66 | enableStateBackup: DEFAULT_CONFIG_VALUES.WORKFLOW.ENABLE_STATE_BACKUP,
67 | maxStateBackups: DEFAULT_CONFIG_VALUES.WORKFLOW.MAX_STATE_BACKUPS,
68 | abortOnMaxAttempts: DEFAULT_CONFIG_VALUES.WORKFLOW.ABORT_ON_MAX_ATTEMPTS
69 | },
70 | storage: {
71 | type: DEFAULT_CONFIG_VALUES.STORAGE.TYPE,
72 | encoding: DEFAULT_CONFIG_VALUES.STORAGE.ENCODING,
73 | enableBackup: false,
74 | maxBackups: DEFAULT_CONFIG_VALUES.STORAGE.MAX_BACKUPS,
75 | enableCompression: false,
76 | atomicOperations: true
77 | },
78 | version: DEFAULT_CONFIG_VALUES.VERSION
79 | };
80 | }
81 |
82 | /**
83 | * Load local project configuration
84 | */
85 | async loadLocalConfig(): Promise<PartialConfiguration | null> {
86 | try {
87 | const configData = await fs.readFile(this.localConfigPath, 'utf-8');
88 | return JSON.parse(configData);
89 | } catch (error: any) {
90 | if (error.code === 'ENOENT') {
91 | // File doesn't exist, return null
92 | console.debug('No local config.json found, using defaults');
93 | return null;
94 | }
95 | throw new TaskMasterError(
96 | 'Failed to load local configuration',
97 | ERROR_CODES.CONFIG_ERROR,
98 | { configPath: this.localConfigPath },
99 | error
100 | );
101 | }
102 | }
103 |
104 | /**
105 | * Load global user configuration
106 | * @future-implementation Full implementation pending
107 | */
108 | async loadGlobalConfig(): Promise<PartialConfiguration | null> {
109 | // TODO: Implement in future PR
110 | // For now, return null to indicate no global config
111 | return null;
112 |
113 | // Future implementation:
114 | // try {
115 | // const configData = await fs.readFile(this.globalConfigPath, 'utf-8');
116 | // return JSON.parse(configData);
117 | // } catch (error: any) {
118 | // if (error.code === 'ENOENT') {
119 | // return null;
120 | // }
121 | // throw new TaskMasterError(
122 | // 'Failed to load global configuration',
123 | // ERROR_CODES.CONFIG_ERROR,
124 | // { configPath: this.globalConfigPath },
125 | // error
126 | // );
127 | // }
128 | }
129 |
130 | /**
131 | * Check if local config exists
132 | */
133 | async hasLocalConfig(): Promise<boolean> {
134 | try {
135 | await fs.access(this.localConfigPath);
136 | return true;
137 | } catch {
138 | return false;
139 | }
140 | }
141 |
142 | /**
143 | * Check if global config exists
144 | */
145 | async hasGlobalConfig(): Promise<boolean> {
146 | try {
147 | await fs.access(this.globalConfigPath);
148 | return true;
149 | } catch {
150 | return false;
151 | }
152 | }
153 | }
154 |
```