This is page 12 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
--------------------------------------------------------------------------------
/packages/tm-core/tests/unit/smoke.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Smoke tests to verify basic package functionality and imports
3 | */
4 |
5 | import {
6 | PlaceholderParser,
7 | PlaceholderStorage,
8 | StorageError,
9 | TaskNotFoundError,
10 | TmCoreError,
11 | ValidationError,
12 | formatDate,
13 | generateTaskId,
14 | isValidTaskId,
15 | name,
16 | version
17 | } from '@tm/core';
18 |
19 | import type {
20 | PlaceholderTask,
21 | TaskId,
22 | TaskPriority,
23 | TaskStatus
24 | } from '@tm/core';
25 |
26 | describe('tm-core smoke tests', () => {
27 | describe('package metadata', () => {
28 | it('should export correct package name and version', () => {
29 | expect(name).toBe('@task-master/tm-core');
30 | expect(version).toBe('1.0.0');
31 | });
32 | });
33 |
34 | describe('utility functions', () => {
35 | it('should generate valid task IDs', () => {
36 | const id1 = generateTaskId();
37 | const id2 = generateTaskId();
38 |
39 | expect(typeof id1).toBe('string');
40 | expect(typeof id2).toBe('string');
41 | expect(id1).not.toBe(id2); // Should be unique
42 | expect(isValidTaskId(id1)).toBe(true);
43 | expect(isValidTaskId('')).toBe(false);
44 | });
45 |
46 | it('should format dates', () => {
47 | const date = new Date('2023-01-01T00:00:00.000Z');
48 | const formatted = formatDate(date);
49 | expect(formatted).toBe('2023-01-01T00:00:00.000Z');
50 | });
51 | });
52 |
53 | describe('placeholder storage', () => {
54 | it('should perform basic storage operations', async () => {
55 | const storage = new PlaceholderStorage();
56 | const testPath = 'test/path';
57 | const testData = 'test data';
58 |
59 | // Initially should not exist
60 | expect(await storage.exists(testPath)).toBe(false);
61 | expect(await storage.read(testPath)).toBe(null);
62 |
63 | // Write and verify
64 | await storage.write(testPath, testData);
65 | expect(await storage.exists(testPath)).toBe(true);
66 | expect(await storage.read(testPath)).toBe(testData);
67 |
68 | // Delete and verify
69 | await storage.delete(testPath);
70 | expect(await storage.exists(testPath)).toBe(false);
71 | });
72 | });
73 |
74 | describe('placeholder parser', () => {
75 | it('should parse simple task lists', async () => {
76 | const parser = new PlaceholderParser();
77 | const content = `
78 | - Task 1
79 | - Task 2
80 | - Task 3
81 | `;
82 |
83 | const isValid = await parser.validate(content);
84 | expect(isValid).toBe(true);
85 |
86 | const tasks = await parser.parse(content);
87 | expect(tasks).toHaveLength(3);
88 | expect(tasks[0]?.title).toBe('Task 1');
89 | expect(tasks[1]?.title).toBe('Task 2');
90 | expect(tasks[2]?.title).toBe('Task 3');
91 |
92 | tasks.forEach((task) => {
93 | expect(task.status).toBe('pending');
94 | expect(task.priority).toBe('medium');
95 | });
96 | });
97 | });
98 |
99 | describe('error classes', () => {
100 | it('should create and throw custom errors', () => {
101 | const baseError = new TmCoreError('Base error');
102 | expect(baseError.name).toBe('TmCoreError');
103 | expect(baseError.message).toBe('Base error');
104 |
105 | const taskNotFound = new TaskNotFoundError('task-123');
106 | expect(taskNotFound.name).toBe('TaskNotFoundError');
107 | expect(taskNotFound.code).toBe('TASK_NOT_FOUND');
108 | expect(taskNotFound.message).toContain('task-123');
109 |
110 | const validationError = new ValidationError('Invalid data');
111 | expect(validationError.name).toBe('ValidationError');
112 | expect(validationError.code).toBe('VALIDATION_ERROR');
113 |
114 | const storageError = new StorageError('Storage failed');
115 | expect(storageError.name).toBe('StorageError');
116 | expect(storageError.code).toBe('STORAGE_ERROR');
117 | });
118 | });
119 |
120 | describe('type definitions', () => {
121 | it('should have correct types available', () => {
122 | // These are compile-time checks that verify types exist
123 | const taskId: TaskId = 'test-id';
124 | const status: TaskStatus = 'pending';
125 | const priority: TaskPriority = 'high';
126 |
127 | const task: PlaceholderTask = {
128 | id: taskId,
129 | title: 'Test Task',
130 | status: status,
131 | priority: priority
132 | };
133 |
134 | expect(task.id).toBe('test-id');
135 | expect(task.status).toBe('pending');
136 | expect(task.priority).toBe('high');
137 | });
138 | });
139 | });
140 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/services/runtime-state-manager.service.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Runtime State Manager Service
3 | * Manages runtime state separate from configuration
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 { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
13 | import { getLogger } from '../../../common/logger/index.js';
14 |
15 | /**
16 | * Runtime state data structure
17 | */
18 | export interface RuntimeState {
19 | /** Currently active tag */
20 | currentTag: string;
21 | /** Last updated timestamp */
22 | lastUpdated?: string;
23 | /** Additional metadata */
24 | metadata?: Record<string, unknown>;
25 | }
26 |
27 | /**
28 | * RuntimeStateManager handles runtime state persistence
29 | * Single responsibility: Runtime state management (separate from config)
30 | */
31 | export class RuntimeStateManager {
32 | private stateFilePath: string;
33 | private currentState: RuntimeState;
34 | private readonly logger = getLogger('RuntimeStateManager');
35 |
36 | constructor(projectRoot: string) {
37 | this.stateFilePath = path.join(projectRoot, '.taskmaster', 'state.json');
38 | this.currentState = {
39 | currentTag: DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG
40 | };
41 | }
42 |
43 | /**
44 | * Load runtime state from disk
45 | */
46 | async loadState(): Promise<RuntimeState> {
47 | try {
48 | const stateData = await fs.readFile(this.stateFilePath, 'utf-8');
49 | const rawState = JSON.parse(stateData);
50 |
51 | // Map legacy field names to current interface
52 | const state: RuntimeState = {
53 | currentTag:
54 | rawState.currentTag ||
55 | rawState.activeTag ||
56 | DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG,
57 | lastUpdated: rawState.lastUpdated,
58 | metadata: rawState.metadata
59 | };
60 |
61 | // Apply environment variable override for current tag
62 | if (process.env.TASKMASTER_TAG) {
63 | state.currentTag = process.env.TASKMASTER_TAG;
64 | }
65 |
66 | this.currentState = state;
67 | return state;
68 | } catch (error: any) {
69 | if (error.code === 'ENOENT') {
70 | // State file doesn't exist, use defaults
71 | this.logger.debug('No state.json found, using default state');
72 |
73 | // Check environment variable
74 | if (process.env.TASKMASTER_TAG) {
75 | this.currentState.currentTag = process.env.TASKMASTER_TAG;
76 | }
77 |
78 | return this.currentState;
79 | }
80 |
81 | // Failed to load, use defaults
82 | this.logger.warn('Failed to load state file:', error.message);
83 | return this.currentState;
84 | }
85 | }
86 |
87 | /**
88 | * Save runtime state to disk
89 | */
90 | async saveState(): Promise<void> {
91 | const stateDir = path.dirname(this.stateFilePath);
92 |
93 | try {
94 | await fs.mkdir(stateDir, { recursive: true });
95 |
96 | const stateToSave = {
97 | ...this.currentState,
98 | lastUpdated: new Date().toISOString()
99 | };
100 |
101 | await fs.writeFile(
102 | this.stateFilePath,
103 | JSON.stringify(stateToSave, null, 2),
104 | 'utf-8'
105 | );
106 | } catch (error) {
107 | throw new TaskMasterError(
108 | 'Failed to save runtime state',
109 | ERROR_CODES.CONFIG_ERROR,
110 | { statePath: this.stateFilePath },
111 | error as Error
112 | );
113 | }
114 | }
115 |
116 | /**
117 | * Get the currently active tag
118 | */
119 | getCurrentTag(): string {
120 | return this.currentState.currentTag;
121 | }
122 |
123 | /**
124 | * Set the current tag
125 | */
126 | async setCurrentTag(tag: string): Promise<void> {
127 | this.currentState.currentTag = tag;
128 | await this.saveState();
129 | }
130 |
131 | /**
132 | * Get current state
133 | */
134 | getState(): RuntimeState {
135 | return { ...this.currentState };
136 | }
137 |
138 | /**
139 | * Update metadata
140 | */
141 | async updateMetadata(metadata: Record<string, unknown>): Promise<void> {
142 | this.currentState.metadata = {
143 | ...this.currentState.metadata,
144 | ...metadata
145 | };
146 | await this.saveState();
147 | }
148 |
149 | /**
150 | * Clear state file
151 | */
152 | async clearState(): Promise<void> {
153 | try {
154 | await fs.unlink(this.stateFilePath);
155 | } catch (error: any) {
156 | if (error.code !== 'ENOENT') {
157 | throw error;
158 | }
159 | }
160 | this.currentState = {
161 | currentTag: DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG
162 | };
163 | }
164 | }
165 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/briefs/utils/url-parser.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview URL Parser
3 | * Utility for parsing URLs to extract organization and brief identifiers
4 | */
5 |
6 | import {
7 | ERROR_CODES,
8 | TaskMasterError
9 | } from '../../../common/errors/task-master-error.js';
10 |
11 | export interface ParsedBriefUrl {
12 | orgSlug: string | null;
13 | briefId: string | null;
14 | }
15 |
16 | /**
17 | * Utility class for parsing brief URLs
18 | * Handles URL formats like: http://localhost:3000/home/{orgSlug}/briefs/{briefId}
19 | */
20 | export class BriefUrlParser {
21 | /**
22 | * Parse a URL and extract org slug and brief ID
23 | *
24 | * @param input - Raw input string (URL, path, or ID)
25 | * @returns Parsed components
26 | */
27 | static parse(input: string): ParsedBriefUrl {
28 | const raw = input?.trim() ?? '';
29 | if (!raw) {
30 | return { orgSlug: null, briefId: null };
31 | }
32 |
33 | // Try parsing as URL
34 | const url = this.parseAsUrl(raw);
35 | const pathToCheck = url ? url.pathname : raw.includes('/') ? raw : null;
36 |
37 | if (!pathToCheck) {
38 | // Not a URL/path, treat as direct ID
39 | return { orgSlug: null, briefId: raw };
40 | }
41 |
42 | // Extract components from path
43 | return this.parsePathComponents(pathToCheck, url);
44 | }
45 |
46 | /**
47 | * Extract organization slug from URL or path
48 | *
49 | * @param input - Raw input string
50 | * @returns Organization slug or null
51 | */
52 | static extractOrgSlug(input: string): string | null {
53 | return this.parse(input).orgSlug;
54 | }
55 |
56 | /**
57 | * Extract brief identifier from URL or path
58 | *
59 | * @param input - Raw input string
60 | * @returns Brief identifier or null
61 | */
62 | static extractBriefId(input: string): string | null {
63 | const parsed = this.parse(input);
64 | return parsed.briefId || input.trim();
65 | }
66 |
67 | /**
68 | * Try to parse input as URL
69 | * Handles both absolute and scheme-less URLs
70 | */
71 | private static parseAsUrl(input: string): URL | null {
72 | try {
73 | return new URL(input);
74 | } catch {}
75 | try {
76 | return new URL(`https://${input}`);
77 | } catch {}
78 | return null;
79 | }
80 |
81 | /**
82 | * Parse path components to extract org slug and brief ID
83 | * Handles patterns like: /home/{orgSlug}/briefs/{briefId}
84 | */
85 | private static parsePathComponents(
86 | path: string,
87 | url: URL | null
88 | ): ParsedBriefUrl {
89 | const parts = path.split('/').filter(Boolean);
90 | const briefsIdx = parts.lastIndexOf('briefs');
91 |
92 | let orgSlug: string | null = null;
93 | let briefId: string | null = null;
94 |
95 | // Extract org slug (segment before 'briefs')
96 | if (briefsIdx > 0) {
97 | orgSlug = parts[briefsIdx - 1] || null;
98 | }
99 |
100 | // Extract brief ID
101 | // Priority: query param > path segment after 'briefs' > last segment (if not 'briefs')
102 | if (url) {
103 | const qId = url.searchParams.get('id') || url.searchParams.get('briefId');
104 | if (qId) {
105 | briefId = qId;
106 | }
107 | }
108 |
109 | if (!briefId && briefsIdx >= 0 && parts.length > briefsIdx + 1) {
110 | briefId = parts[briefsIdx + 1];
111 | }
112 |
113 | // Only use last segment as fallback if path doesn't end with 'briefs'
114 | // This prevents treating '/home/org/briefs' as briefId='briefs'
115 | if (
116 | !briefId &&
117 | parts.length > 0 &&
118 | !(briefsIdx >= 0 && briefsIdx === parts.length - 1)
119 | ) {
120 | briefId = parts[parts.length - 1];
121 | }
122 |
123 | return { orgSlug, briefId };
124 | }
125 |
126 | /**
127 | * Validate that required components are present
128 | *
129 | * @param parsed - Parsed URL components
130 | * @param requireOrg - Whether org slug is required
131 | * @param requireBrief - Whether brief ID is required
132 | * @throws TaskMasterError if required components are missing
133 | */
134 | static validate(
135 | parsed: ParsedBriefUrl,
136 | options: { requireOrg?: boolean; requireBrief?: boolean } = {}
137 | ): void {
138 | if (options.requireOrg && !parsed.orgSlug) {
139 | throw new TaskMasterError(
140 | 'Organization slug could not be extracted from input',
141 | ERROR_CODES.VALIDATION_ERROR
142 | );
143 | }
144 |
145 | if (options.requireBrief && !parsed.briefId) {
146 | throw new TaskMasterError(
147 | 'Brief identifier could not be extracted from input',
148 | ERROR_CODES.VALIDATION_ERROR
149 | );
150 | }
151 | }
152 | }
153 |
```
--------------------------------------------------------------------------------
/.taskmaster/docs/research/2025-06-14_test-the-fix-for-duplicate-saves-final-test.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Research Session
3 | query: "test the fix for duplicate saves - final test"
4 | date: 6/14/2025
5 | time: 6:10:00 PM
6 | timestamp: 2025-06-14T22:10:00.394Z
7 | exchanges: 1
8 | ---
9 |
10 | # Research Session
11 |
12 | ## Initial Query
13 |
14 | **Question:** test the fix for duplicate saves - final test
15 |
16 | **Response:**
17 |
18 | ## Steps to Test the Fix for Duplicate Saves
19 |
20 | **1. Prepare a Clean Test Environment**
21 | - Ensure the tasks.json file and any related data stores are in a known, clean state with no pre-existing duplicate entries.
22 | - Back up the current tasks.json in case rollback is needed.
23 |
24 | **2. Define Test Scenarios**
25 | - Attempt to save a new task with unique data.
26 | - Attempt to save a task with the same ID or unique identifier as an existing task (simulate a duplicate).
27 | - Attempt to save a task with the same title or content but a different ID (to test if the fix is based on ID or content).
28 | - Simultaneously trigger multiple save operations (if the system supports concurrency) to test race conditions.
29 |
30 | **3. Execute Manual and Automated Tests**
31 | - Use the application's UI or API to create and save tasks as per the scenarios above.
32 | - For each save operation, check the tasks.json file or the data store to verify:
33 | - No duplicate entries are created.
34 | - Existing tasks are not overwritten unless intended (e.g., for updates).
35 | - The system returns appropriate error messages or warnings when a duplicate save is attempted.
36 |
37 | **4. Validate System Behavior**
38 | - Confirm that after all test operations, each task in tasks.json is unique according to the defined criteria (ID, title, or other unique fields).
39 | - If the system is designed to merge duplicates, verify that merging occurs as expected.
40 | - If the system is designed to reject duplicates, ensure that the rejection is clear and no duplicate is present.
41 |
42 | **5. Test Edge Cases**
43 | - Attempt to save tasks with minor variations (e.g., whitespace differences, case sensitivity) to ensure the duplicate detection logic is robust.
44 | - Test with large numbers of tasks to ensure performance and correctness at scale.
45 |
46 | **6. Review Logs and Error Handling**
47 | - Check application logs for any errors or warnings related to duplicate saves.
48 | - Ensure that error handling is user-friendly and provides actionable feedback.
49 |
50 | **7. Regression Testing**
51 | - Run the full suite of task operations (create, update, delete) to ensure the fix for duplicate saves does not introduce new issues elsewhere in the system.
52 |
53 | ## Additional Best Practices
54 |
55 | - Communicate test results with the team to confirm the fix is effective and to prevent future regressions[1][2][5].
56 | - Consider implementing automated duplicate detection as part of the save operation to prevent future issues[2][3].
57 | - Document the test cases and results for future reference and audits.
58 |
59 | ## Example Test Case Table
60 |
61 | | Test Scenario | Expected Result | Actual Result | Pass/Fail |
62 | |--------------------------------------|-------------------------------|---------------|-----------|
63 | | Save unique task | Task saved | | |
64 | | Save duplicate task (same ID) | Duplicate rejected/merged | | |
65 | | Save duplicate task (same title) | Duplicate rejected/merged | | |
66 | | Simultaneous saves (race condition) | Only one unique task present | | |
67 | | Save with minor variations | No false positives/negatives | | |
68 |
69 | Fill in the "Actual Result" and "Pass/Fail" columns during testing.
70 |
71 | ## Action Items
72 |
73 | - Complete all test scenarios above.
74 | - Document any issues found and retest after fixes.
75 | - Confirm with stakeholders before closing the issue.
76 |
77 |
78 | ---
79 |
80 | *Generated by Task Master Research Command*
81 | *Timestamp: 2025-06-14T22:10:00.394Z*
82 |
```
--------------------------------------------------------------------------------
/src/progress/tracker-ui.js:
--------------------------------------------------------------------------------
```javascript
1 | import chalk from 'chalk';
2 |
3 | /**
4 | * Factory for creating progress bar elements
5 | */
6 | class ProgressBarFactory {
7 | constructor(multibar) {
8 | if (!multibar) {
9 | throw new Error('Multibar instance is required');
10 | }
11 | this.multibar = multibar;
12 | }
13 |
14 | /**
15 | * Creates a progress bar with the given format
16 | */
17 | createBar(format, payload = {}) {
18 | if (typeof format !== 'string') {
19 | throw new Error('Format must be a string');
20 | }
21 |
22 | const bar = this.multibar.create(
23 | 1, // total
24 | 1, // current
25 | {},
26 | {
27 | format,
28 | barsize: 1,
29 | hideCursor: true,
30 | clearOnComplete: false
31 | }
32 | );
33 |
34 | bar.update(1, payload);
35 | return bar;
36 | }
37 |
38 | /**
39 | * Creates a header with borders
40 | */
41 | createHeader(headerFormat, borderFormat) {
42 | this.createBar(borderFormat); // Top border
43 | this.createBar(headerFormat); // Header
44 | this.createBar(borderFormat); // Bottom border
45 | }
46 |
47 | /**
48 | * Creates a data row
49 | */
50 | createRow(rowFormat, payload) {
51 | if (!payload || typeof payload !== 'object') {
52 | throw new Error('Payload must be an object');
53 | }
54 | return this.createBar(rowFormat, payload);
55 | }
56 |
57 | /**
58 | * Creates a border element
59 | */
60 | createBorder(borderFormat) {
61 | return this.createBar(borderFormat);
62 | }
63 | }
64 |
65 | /**
66 | * Creates a bordered header for progress tables.
67 | * @param {Object} multibar - The multibar instance.
68 | * @param {string} headerFormat - Format string for the header row.
69 | * @param {string} borderFormat - Format string for the top and bottom borders.
70 | * @returns {void}
71 | */
72 | export function createProgressHeader(multibar, headerFormat, borderFormat) {
73 | const factory = new ProgressBarFactory(multibar);
74 | factory.createHeader(headerFormat, borderFormat);
75 | }
76 |
77 | /**
78 | * Creates a formatted data row for progress tables.
79 | * @param {Object} multibar - The multibar instance.
80 | * @param {string} rowFormat - Format string for the row.
81 | * @param {Object} payload - Data payload for the row format.
82 | * @returns {void}
83 | */
84 | export function createProgressRow(multibar, rowFormat, payload) {
85 | const factory = new ProgressBarFactory(multibar);
86 | factory.createRow(rowFormat, payload);
87 | }
88 |
89 | /**
90 | * Creates a border row for progress tables.
91 | * @param {Object} multibar - The multibar instance.
92 | * @param {string} borderFormat - Format string for the border.
93 | * @returns {void}
94 | */
95 | export function createBorder(multibar, borderFormat) {
96 | const factory = new ProgressBarFactory(multibar);
97 | factory.createBorder(borderFormat);
98 | }
99 |
100 | /**
101 | * Builder for creating progress tables with consistent formatting
102 | */
103 | export class ProgressTableBuilder {
104 | constructor(multibar) {
105 | this.factory = new ProgressBarFactory(multibar);
106 | this.borderStyle = '─';
107 | this.columnSeparator = '|';
108 | }
109 |
110 | /**
111 | * Shows a formatted table header
112 | */
113 | showHeader(columns = null) {
114 | // Default columns for task display
115 | const defaultColumns = [
116 | { text: 'TASK', width: 6 },
117 | { text: 'PRI', width: 5 },
118 | { text: 'TITLE', width: 64 }
119 | ];
120 |
121 | const cols = columns || defaultColumns;
122 | const headerText = ' ' + cols.map((c) => c.text).join(' | ') + ' ';
123 | const borderLine = this.createBorderLine(cols.map((c) => c.width));
124 |
125 | this.factory.createHeader(headerText, borderLine);
126 | return this;
127 | }
128 |
129 | /**
130 | * Creates a border line based on column widths
131 | */
132 | createBorderLine(columnWidths) {
133 | return columnWidths
134 | .map((width) => this.borderStyle.repeat(width))
135 | .join('─┼─');
136 | }
137 |
138 | /**
139 | * Adds a task row to the table
140 | */
141 | addTaskRow(taskId, priority, title) {
142 | const format = ` ${taskId} | ${priority} | {title}`;
143 | this.factory.createRow(format, { title });
144 |
145 | // Add separator after each row
146 | const borderLine = '------+-----+' + '─'.repeat(64);
147 | this.factory.createBorder(borderLine);
148 | return this;
149 | }
150 |
151 | /**
152 | * Creates a summary row
153 | */
154 | addSummaryRow(label, value) {
155 | const format = ` ${label}: {value}`;
156 | this.factory.createRow(format, { value });
157 | return this;
158 | }
159 | }
160 |
```
--------------------------------------------------------------------------------
/tests/unit/mcp/tools/move-task-cross-tag-options.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mocks
4 | const mockFindTasksPath = jest
5 | .fn()
6 | .mockReturnValue('/test/path/.taskmaster/tasks/tasks.json');
7 | jest.unstable_mockModule(
8 | '../../../../mcp-server/src/core/utils/path-utils.js',
9 | () => ({
10 | findTasksPath: mockFindTasksPath
11 | })
12 | );
13 |
14 | const mockEnableSilentMode = jest.fn();
15 | const mockDisableSilentMode = jest.fn();
16 | jest.unstable_mockModule('../../../../scripts/modules/utils.js', () => ({
17 | enableSilentMode: mockEnableSilentMode,
18 | disableSilentMode: mockDisableSilentMode
19 | }));
20 |
21 | // Spyable mock for moveTasksBetweenTags
22 | const mockMoveTasksBetweenTags = jest.fn();
23 | jest.unstable_mockModule(
24 | '../../../../scripts/modules/task-manager/move-task.js',
25 | () => ({
26 | moveTasksBetweenTags: mockMoveTasksBetweenTags
27 | })
28 | );
29 |
30 | // Import after mocks
31 | const { moveTaskCrossTagDirect } = await import(
32 | '../../../../mcp-server/src/core/direct-functions/move-task-cross-tag.js'
33 | );
34 |
35 | describe('MCP Cross-Tag Move Direct Function - options & suggestions', () => {
36 | const mockLog = { info: jest.fn(), warn: jest.fn(), error: jest.fn() };
37 |
38 | beforeEach(() => {
39 | jest.clearAllMocks();
40 | });
41 |
42 | it('passes only withDependencies/ignoreDependencies (no force) to core', async () => {
43 | // Arrange: make core throw tag validation after call to capture params
44 | mockMoveTasksBetweenTags.mockImplementation(() => {
45 | const err = new Error('Source tag "invalid" not found or invalid');
46 | err.code = 'INVALID_SOURCE_TAG';
47 | throw err;
48 | });
49 |
50 | // Act
51 | await moveTaskCrossTagDirect(
52 | {
53 | sourceIds: '1,2',
54 | sourceTag: 'backlog',
55 | targetTag: 'in-progress',
56 | withDependencies: true,
57 | projectRoot: '/test'
58 | },
59 | mockLog
60 | );
61 |
62 | // Assert options argument (5th param)
63 | expect(mockMoveTasksBetweenTags).toHaveBeenCalled();
64 | const args = mockMoveTasksBetweenTags.mock.calls[0];
65 | const moveOptions = args[4];
66 | expect(moveOptions).toEqual({
67 | withDependencies: true,
68 | ignoreDependencies: false
69 | });
70 | expect('force' in moveOptions).toBe(false);
71 | });
72 |
73 | it('returns conflict suggestions on cross-tag dependency conflicts', async () => {
74 | // Arrange: core throws cross-tag dependency conflicts
75 | mockMoveTasksBetweenTags.mockImplementation(() => {
76 | const err = new Error(
77 | 'Cannot move tasks: 2 cross-tag dependency conflicts found'
78 | );
79 | err.code = 'CROSS_TAG_DEPENDENCY_CONFLICTS';
80 | throw err;
81 | });
82 |
83 | // Act
84 | const result = await moveTaskCrossTagDirect(
85 | {
86 | sourceIds: '1',
87 | sourceTag: 'backlog',
88 | targetTag: 'in-progress',
89 | projectRoot: '/test'
90 | },
91 | mockLog
92 | );
93 |
94 | // Assert
95 | expect(result.success).toBe(false);
96 | expect(result.error.code).toBe('CROSS_TAG_DEPENDENCY_CONFLICT');
97 | expect(Array.isArray(result.error.suggestions)).toBe(true);
98 | // Key suggestions
99 | const s = result.error.suggestions.join(' ');
100 | expect(s).toContain('--with-dependencies');
101 | expect(s).toContain('--ignore-dependencies');
102 | expect(s).toContain('validate-dependencies');
103 | expect(s).toContain('Move dependencies first');
104 | });
105 |
106 | it('returns ID collision suggestions when target tag already has the ID', async () => {
107 | // Arrange: core throws TASK_ALREADY_EXISTS structured error
108 | mockMoveTasksBetweenTags.mockImplementation(() => {
109 | const err = new Error(
110 | 'Task 1 already exists in target tag "in-progress"'
111 | );
112 | err.code = 'TASK_ALREADY_EXISTS';
113 | throw err;
114 | });
115 |
116 | // Act
117 | const result = await moveTaskCrossTagDirect(
118 | {
119 | sourceIds: '1',
120 | sourceTag: 'backlog',
121 | targetTag: 'in-progress',
122 | projectRoot: '/test'
123 | },
124 | mockLog
125 | );
126 |
127 | // Assert
128 | expect(result.success).toBe(false);
129 | expect(result.error.code).toBe('TASK_ALREADY_EXISTS');
130 | const joined = (result.error.suggestions || []).join(' ');
131 | expect(joined).toContain('different target tag');
132 | expect(joined).toContain('different set of IDs');
133 | expect(joined).toContain('within-tag');
134 | });
135 | });
136 |
```
--------------------------------------------------------------------------------
/.github/workflows/weekly-metrics-discord.yml:
--------------------------------------------------------------------------------
```yaml
1 | name: Weekly Metrics to Discord
2 | # description: Sends weekly metrics summary to Discord channel
3 |
4 | on:
5 | schedule:
6 | - cron: "0 9 * * 1" # Every Monday at 9 AM
7 | workflow_dispatch:
8 |
9 | permissions:
10 | contents: read
11 | issues: read
12 | pull-requests: read
13 |
14 | jobs:
15 | weekly-metrics:
16 | runs-on: ubuntu-latest
17 | env:
18 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }}
19 | steps:
20 | - name: Checkout repository
21 | uses: actions/checkout@v4
22 |
23 | - name: Setup Node.js
24 | uses: actions/setup-node@v4
25 | with:
26 | node-version: '20'
27 |
28 | - name: Get dates for last 14 days
29 | run: |
30 | set -Eeuo pipefail
31 | # Last 14 days
32 | first_day=$(date -d "14 days ago" +%Y-%m-%d)
33 | last_day=$(date +%Y-%m-%d)
34 |
35 | echo "first_day=$first_day" >> $GITHUB_ENV
36 | echo "last_day=$last_day" >> $GITHUB_ENV
37 | echo "week_of=$(date -d '7 days ago' +'Week of %B %d, %Y')" >> $GITHUB_ENV
38 | echo "date_range=Past 14 days ($first_day to $last_day)" >> $GITHUB_ENV
39 |
40 | - name: Generate issue metrics
41 | uses: github/issue-metrics@v3
42 | env:
43 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44 | SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}"
45 | HIDE_TIME_TO_ANSWER: true
46 | HIDE_LABEL_METRICS: false
47 | OUTPUT_FILE: issue_metrics.md
48 |
49 | - name: Generate PR created metrics
50 | uses: github/issue-metrics@v3
51 | env:
52 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
53 | SEARCH_QUERY: "repo:${{ github.repository }} is:pr created:${{ env.first_day }}..${{ env.last_day }}"
54 | OUTPUT_FILE: pr_created_metrics.md
55 |
56 | - name: Generate PR merged metrics
57 | uses: github/issue-metrics@v3
58 | env:
59 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
60 | SEARCH_QUERY: "repo:${{ github.repository }} is:pr is:merged merged:${{ env.first_day }}..${{ env.last_day }}"
61 | OUTPUT_FILE: pr_merged_metrics.md
62 |
63 | - name: Debug generated metrics
64 | run: |
65 | set -Eeuo pipefail
66 | echo "Listing markdown files in workspace:"
67 | ls -la *.md || true
68 | for f in issue_metrics.md pr_created_metrics.md pr_merged_metrics.md; do
69 | if [ -f "$f" ]; then
70 | echo "== $f (first 10 lines) =="
71 | head -n 10 "$f"
72 | else
73 | echo "Missing $f"
74 | fi
75 | done
76 |
77 | - name: Parse metrics
78 | id: metrics
79 | run: node .github/scripts/parse-metrics.mjs
80 |
81 | - name: Send to Discord
82 | uses: sarisia/actions-status-discord@v1
83 | if: env.DISCORD_WEBHOOK != ''
84 | with:
85 | webhook: ${{ env.DISCORD_WEBHOOK }}
86 | status: Success
87 | title: "📊 Weekly Metrics Report"
88 | description: |
89 | **${{ env.week_of }}**
90 | *${{ env.date_range }}*
91 |
92 | **🎯 Issues**
93 | • Created: ${{ steps.metrics.outputs.issues_created }}
94 | • Closed: ${{ steps.metrics.outputs.issues_closed }}
95 | • Avg Response Time: ${{ steps.metrics.outputs.issue_avg_first_response }}
96 | • Avg Time to Close: ${{ steps.metrics.outputs.issue_avg_time_to_close }}
97 |
98 | **🔀 Pull Requests**
99 | • Created: ${{ steps.metrics.outputs.prs_created }}
100 | • Merged: ${{ steps.metrics.outputs.prs_merged }}
101 | • Avg Response Time: ${{ steps.metrics.outputs.pr_avg_first_response }}
102 | • Avg Time to Merge: ${{ steps.metrics.outputs.pr_avg_merge_time }}
103 |
104 | **📈 Visual Analytics**
105 | https://repobeats.axiom.co/api/embed/b439f28f0ab5bd7a2da19505355693cd2c55bfd4.svg
106 | color: 0x58AFFF
107 | username: Task Master Metrics Bot
108 | avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png
109 |
```
--------------------------------------------------------------------------------
/apps/extension/src/webview/utils/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Webview Logger Utility
3 | * Provides conditional logging based on environment
4 | */
5 |
6 | type LogLevel = 'log' | 'warn' | 'error' | 'debug' | 'info';
7 |
8 | interface LogEntry {
9 | level: LogLevel;
10 | message: string;
11 | data?: any;
12 | timestamp: number;
13 | }
14 |
15 | class WebviewLogger {
16 | private static instance: WebviewLogger;
17 | private enabled: boolean;
18 | private logHistory: LogEntry[] = [];
19 | private maxHistorySize = 100;
20 |
21 | private constructor() {
22 | // Enable logging in development, disable in production
23 | // Check for development mode via various indicators
24 | this.enabled = this.isDevelopment();
25 | }
26 |
27 | static getInstance(): WebviewLogger {
28 | if (!WebviewLogger.instance) {
29 | WebviewLogger.instance = new WebviewLogger();
30 | }
31 | return WebviewLogger.instance;
32 | }
33 |
34 | private isDevelopment(): boolean {
35 | // Check various indicators for development mode
36 | // VS Code webviews don't have process.env, so we check other indicators
37 | return (
38 | // Check if running in localhost (development server)
39 | window.location.hostname === 'localhost' ||
40 | // Check for development query parameter
41 | window.location.search.includes('debug=true') ||
42 | // Check for VS Code development mode indicator
43 | (window as any).__VSCODE_DEV_MODE__ === true ||
44 | // Default to false in production
45 | false
46 | );
47 | }
48 |
49 | private addToHistory(entry: LogEntry): void {
50 | this.logHistory.push(entry);
51 | if (this.logHistory.length > this.maxHistorySize) {
52 | this.logHistory.shift();
53 | }
54 | }
55 |
56 | private logMessage(level: LogLevel, message: string, ...args: any[]): void {
57 | const entry: LogEntry = {
58 | level,
59 | message,
60 | data: args.length > 0 ? args : undefined,
61 | timestamp: Date.now()
62 | };
63 |
64 | this.addToHistory(entry);
65 |
66 | if (!this.enabled) {
67 | return;
68 | }
69 |
70 | // Format the message with timestamp
71 | const timestamp = new Date().toISOString();
72 | const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
73 |
74 | // Use appropriate console method
75 | switch (level) {
76 | case 'error':
77 | console.error(prefix, message, ...args);
78 | break;
79 | case 'warn':
80 | console.warn(prefix, message, ...args);
81 | break;
82 | case 'debug':
83 | console.debug(prefix, message, ...args);
84 | break;
85 | case 'info':
86 | console.info(prefix, message, ...args);
87 | break;
88 | default:
89 | console.log(prefix, message, ...args);
90 | }
91 | }
92 |
93 | log(message: string, ...args: any[]): void {
94 | this.logMessage('log', message, ...args);
95 | }
96 |
97 | error(message: string, ...args: any[]): void {
98 | // Always log errors, even in production
99 | const entry: LogEntry = {
100 | level: 'error',
101 | message,
102 | data: args.length > 0 ? args : undefined,
103 | timestamp: Date.now()
104 | };
105 | this.addToHistory(entry);
106 | console.error(`[${new Date().toISOString()}] [ERROR]`, message, ...args);
107 | }
108 |
109 | warn(message: string, ...args: any[]): void {
110 | this.logMessage('warn', message, ...args);
111 | }
112 |
113 | debug(message: string, ...args: any[]): void {
114 | this.logMessage('debug', message, ...args);
115 | }
116 |
117 | info(message: string, ...args: any[]): void {
118 | this.logMessage('info', message, ...args);
119 | }
120 |
121 | // Enable/disable logging dynamically
122 | setEnabled(enabled: boolean): void {
123 | this.enabled = enabled;
124 | if (enabled) {
125 | console.log('[WebviewLogger] Logging enabled');
126 | }
127 | }
128 |
129 | // Get log history (useful for debugging)
130 | getHistory(): LogEntry[] {
131 | return [...this.logHistory];
132 | }
133 |
134 | // Clear log history
135 | clearHistory(): void {
136 | this.logHistory = [];
137 | }
138 |
139 | // Export logs as string (useful for bug reports)
140 | exportLogs(): string {
141 | return this.logHistory
142 | .map((entry) => {
143 | const timestamp = new Date(entry.timestamp).toISOString();
144 | const data = entry.data ? JSON.stringify(entry.data) : '';
145 | return `[${timestamp}] [${entry.level.toUpperCase()}] ${entry.message} ${data}`;
146 | })
147 | .join('\n');
148 | }
149 | }
150 |
151 | // Export singleton instance
152 | export const logger = WebviewLogger.getInstance();
153 |
154 | // Export type for use in other files
155 | export type { WebviewLogger };
156 |
```
--------------------------------------------------------------------------------
/apps/extension/src/services/terminal-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Terminal Manager - Handles task execution in VS Code terminals
3 | * Uses @tm/core for consistent task management with the CLI
4 | */
5 |
6 | import * as vscode from 'vscode';
7 | import { createTmCore, type TmCore } from '@tm/core';
8 | import type { ExtensionLogger } from '../utils/logger';
9 |
10 | export interface TerminalExecutionOptions {
11 | taskId: string;
12 | taskTitle: string;
13 | tag?: string;
14 | }
15 |
16 | export interface TerminalExecutionResult {
17 | success: boolean;
18 | error?: string;
19 | terminalName?: string;
20 | }
21 |
22 | export class TerminalManager {
23 | private terminals = new Map<string, vscode.Terminal>();
24 | private tmCore?: TmCore;
25 |
26 | constructor(
27 | private context: vscode.ExtensionContext,
28 | private logger: ExtensionLogger
29 | ) {}
30 |
31 | /**
32 | * Execute a task in a new VS Code terminal with Claude
33 | * Uses @tm/core for consistent task management with the CLI
34 | */
35 | async executeTask(
36 | options: TerminalExecutionOptions
37 | ): Promise<TerminalExecutionResult> {
38 | const { taskTitle, tag } = options;
39 | // Ensure taskId is always a string
40 | const taskId = String(options.taskId);
41 |
42 | this.logger.log(
43 | `Starting task execution for ${taskId}: ${taskTitle}${tag ? ` (tag: ${tag})` : ''}`
44 | );
45 | this.logger.log(`TaskId type: ${typeof taskId}, value: ${taskId}`);
46 |
47 | try {
48 | // Initialize tm-core if needed
49 | await this.initializeCore();
50 |
51 | // Use tm-core to start the task (same as CLI)
52 | const startResult = await this.tmCore!.tasks.start(taskId, {
53 | dryRun: false,
54 | force: false,
55 | updateStatus: true
56 | });
57 |
58 | if (!startResult.started || !startResult.executionOutput) {
59 | throw new Error(
60 | startResult.error || 'Failed to start task with tm-core'
61 | );
62 | }
63 |
64 | // Create terminal with custom TaskMaster icon
65 | const terminalName = `Task ${taskId}: ${taskTitle}`;
66 | const terminal = this.createTerminal(terminalName);
67 |
68 | // Store terminal reference for potential cleanup
69 | this.terminals.set(taskId, terminal);
70 |
71 | // Show terminal and run Claude command
72 | terminal.show();
73 | const command = `claude "${startResult.executionOutput}"`;
74 | terminal.sendText(command);
75 |
76 | this.logger.log(`Launched Claude for task ${taskId} using tm-core`);
77 |
78 | return {
79 | success: true,
80 | terminalName
81 | };
82 | } catch (error) {
83 | this.logger.error('Failed to execute task:', error);
84 | return {
85 | success: false,
86 | error: error instanceof Error ? error.message : 'Unknown error'
87 | };
88 | }
89 | }
90 |
91 | /**
92 | * Create a new terminal with TaskMaster branding
93 | */
94 | private createTerminal(name: string): vscode.Terminal {
95 | const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
96 |
97 | return vscode.window.createTerminal({
98 | name,
99 | cwd: workspaceRoot,
100 | iconPath: new vscode.ThemeIcon('play') // Use a VS Code built-in icon for now
101 | });
102 | }
103 |
104 | /**
105 | * Initialize TaskMaster Core (same as CLI)
106 | */
107 | private async initializeCore(): Promise<void> {
108 | if (!this.tmCore) {
109 | const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
110 | if (!workspaceRoot) {
111 | throw new Error('No workspace folder found');
112 | }
113 | this.tmCore = await createTmCore({ projectPath: workspaceRoot });
114 | }
115 | }
116 |
117 | /**
118 | * Get terminal by task ID (if still active)
119 | */
120 | getTerminalByTaskId(taskId: string): vscode.Terminal | undefined {
121 | return this.terminals.get(taskId);
122 | }
123 |
124 | /**
125 | * Clean up terminated terminals
126 | */
127 | cleanupTerminal(taskId: string): void {
128 | const terminal = this.terminals.get(taskId);
129 | if (terminal) {
130 | this.terminals.delete(taskId);
131 | }
132 | }
133 |
134 | /**
135 | * Dispose all managed terminals and clean up tm-core
136 | */
137 | async dispose(): Promise<void> {
138 | this.terminals.forEach((terminal) => {
139 | try {
140 | terminal.dispose();
141 | } catch (error) {
142 | this.logger.error('Failed to dispose terminal:', error);
143 | }
144 | });
145 | this.terminals.clear();
146 |
147 | // Clear tm-core reference (no explicit cleanup needed)
148 | if (this.tmCore) {
149 | this.tmCore = undefined;
150 | }
151 | }
152 | }
153 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/auth/managers/auth-manager.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Tests for AuthManager singleton behavior
3 | */
4 |
5 | import { beforeEach, describe, expect, it, vi } from 'vitest';
6 |
7 | // Mock the logger to verify warnings (must be hoisted before SUT import)
8 | const mockLogger = {
9 | warn: vi.fn(),
10 | info: vi.fn(),
11 | debug: vi.fn(),
12 | error: vi.fn()
13 | };
14 |
15 | vi.mock('../logger/index.js', () => ({
16 | getLogger: () => mockLogger
17 | }));
18 |
19 | // Spy on CredentialStore constructor to verify config propagation
20 | const CredentialStoreSpy = vi.fn();
21 | vi.mock('./credential-store.js', () => {
22 | return {
23 | CredentialStore: class {
24 | static getInstance(config?: any) {
25 | return new (this as any)(config);
26 | }
27 | static resetInstance() {
28 | // Mock reset instance method
29 | }
30 | constructor(config: any) {
31 | CredentialStoreSpy(config);
32 | }
33 | getCredentials(_options?: any) {
34 | return null;
35 | }
36 | saveCredentials() {}
37 | clearCredentials() {}
38 | hasCredentials() {
39 | return false;
40 | }
41 | }
42 | };
43 | });
44 |
45 | // Mock OAuthService to avoid side effects
46 | vi.mock('./oauth-service.js', () => {
47 | return {
48 | OAuthService: class {
49 | constructor() {}
50 | authenticate() {
51 | return Promise.resolve({});
52 | }
53 | getAuthorizationUrl() {
54 | return null;
55 | }
56 | }
57 | };
58 | });
59 |
60 | // Mock SupabaseAuthClient to avoid side effects
61 | vi.mock('../clients/supabase-client.js', () => {
62 | return {
63 | SupabaseAuthClient: class {
64 | constructor() {}
65 | refreshSession() {
66 | return Promise.resolve({});
67 | }
68 | signOut() {
69 | return Promise.resolve();
70 | }
71 | }
72 | };
73 | });
74 |
75 | // Import SUT after mocks
76 | import { AuthManager } from './auth-manager.js';
77 |
78 | describe('AuthManager Singleton', () => {
79 | beforeEach(() => {
80 | // Reset singleton before each test
81 | AuthManager.resetInstance();
82 | vi.clearAllMocks();
83 | CredentialStoreSpy.mockClear();
84 | });
85 |
86 | it('should return the same instance on multiple calls', () => {
87 | const instance1 = AuthManager.getInstance();
88 | const instance2 = AuthManager.getInstance();
89 |
90 | expect(instance1).toBe(instance2);
91 | });
92 |
93 | it('should use config on first call', async () => {
94 | const config = {
95 | baseUrl: 'https://test.auth.com',
96 | configDir: '/test/config',
97 | configFile: '/test/config/auth.json'
98 | };
99 |
100 | const instance = AuthManager.getInstance(config);
101 | expect(instance).toBeDefined();
102 |
103 | // Assert that CredentialStore was constructed with the provided config
104 | expect(CredentialStoreSpy).toHaveBeenCalledTimes(1);
105 | expect(CredentialStoreSpy).toHaveBeenCalledWith(config);
106 |
107 | // Verify the config is passed to internal components through observable behavior
108 | // getCredentials would look in the configured file path
109 | const credentials = await instance.getCredentials();
110 | expect(credentials).toBeNull(); // File doesn't exist, but config was propagated correctly
111 | });
112 |
113 | it('should warn when config is provided after initialization', () => {
114 | // Clear previous calls
115 | mockLogger.warn.mockClear();
116 |
117 | // First call with config
118 | AuthManager.getInstance({ baseUrl: 'https://first.auth.com' });
119 |
120 | // Second call with different config
121 | AuthManager.getInstance({ baseUrl: 'https://second.auth.com' });
122 |
123 | // Verify warning was logged
124 | expect(mockLogger.warn).toHaveBeenCalledWith(
125 | expect.stringMatching(/config.*after initialization.*ignored/i)
126 | );
127 | });
128 |
129 | it('should not warn when no config is provided after initialization', () => {
130 | // Clear previous calls
131 | mockLogger.warn.mockClear();
132 |
133 | // First call with config
134 | AuthManager.getInstance({ configDir: '/test/config' });
135 |
136 | // Second call without config
137 | AuthManager.getInstance();
138 |
139 | // Verify no warning was logged
140 | expect(mockLogger.warn).not.toHaveBeenCalled();
141 | });
142 |
143 | it('should allow resetting the instance', () => {
144 | const instance1 = AuthManager.getInstance();
145 |
146 | // Reset the instance
147 | AuthManager.resetInstance();
148 |
149 | // Get new instance
150 | const instance2 = AuthManager.getInstance();
151 |
152 | // They should be different instances
153 | expect(instance1).not.toBe(instance2);
154 | });
155 | });
156 |
```
--------------------------------------------------------------------------------
/packages/ai-sdk-provider-grok-cli/src/message-converter.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Message format conversion utilities for Grok CLI provider
3 | */
4 |
5 | import type { GrokCliMessage, GrokCliResponse } from './types.js';
6 |
7 | /**
8 | * AI SDK message type (simplified interface)
9 | */
10 | interface AISDKMessage {
11 | role: string;
12 | content:
13 | | string
14 | | Array<{ type: string; text?: string }>
15 | | { text?: string; [key: string]: unknown };
16 | }
17 |
18 | /**
19 | * Convert AI SDK messages to Grok CLI compatible format
20 | * @param messages - AI SDK message array
21 | * @returns Grok CLI compatible messages
22 | */
23 | export function convertToGrokCliMessages(
24 | messages: AISDKMessage[]
25 | ): GrokCliMessage[] {
26 | return messages.map((message) => {
27 | // Handle different message content types
28 | let content = '';
29 |
30 | if (typeof message.content === 'string') {
31 | content = message.content;
32 | } else if (Array.isArray(message.content)) {
33 | // Handle multi-part content (text and images)
34 | content = message.content
35 | .filter((part) => part.type === 'text')
36 | .map((part) => part.text || '')
37 | .join('\n');
38 | } else if (message.content && typeof message.content === 'object') {
39 | // Handle object content
40 | content = message.content.text || JSON.stringify(message.content);
41 | }
42 |
43 | return {
44 | role: message.role,
45 | content: content.trim()
46 | };
47 | });
48 | }
49 |
50 | /**
51 | * Convert Grok CLI response to AI SDK format
52 | * @param responseText - Raw response text from Grok CLI (JSONL format)
53 | * @returns AI SDK compatible response object
54 | */
55 | export function convertFromGrokCliResponse(responseText: string): {
56 | text: string;
57 | usage?: {
58 | promptTokens: number;
59 | completionTokens: number;
60 | totalTokens: number;
61 | };
62 | } {
63 | try {
64 | // Grok CLI outputs JSONL format - each line is a separate JSON message
65 | const lines = responseText
66 | .trim()
67 | .split('\n')
68 | .filter((line) => line.trim());
69 |
70 | // Parse each line as JSON and find assistant messages
71 | const messages: GrokCliResponse[] = [];
72 | for (const line of lines) {
73 | try {
74 | const message = JSON.parse(line) as GrokCliResponse;
75 | messages.push(message);
76 | } catch (parseError) {
77 | // Skip invalid JSON lines
78 | continue;
79 | }
80 | }
81 |
82 | // Find the last assistant message
83 | const assistantMessage = messages
84 | .filter((msg) => msg.role === 'assistant')
85 | .pop();
86 |
87 | if (assistantMessage && assistantMessage.content) {
88 | return {
89 | text: assistantMessage.content,
90 | usage: assistantMessage.usage
91 | ? {
92 | promptTokens: assistantMessage.usage.prompt_tokens || 0,
93 | completionTokens: assistantMessage.usage.completion_tokens || 0,
94 | totalTokens: assistantMessage.usage.total_tokens || 0
95 | }
96 | : undefined
97 | };
98 | }
99 |
100 | // Fallback: if no assistant message found, return the raw text
101 | return {
102 | text: responseText.trim(),
103 | usage: undefined
104 | };
105 | } catch (error) {
106 | // If parsing fails completely, treat as plain text response
107 | return {
108 | text: responseText.trim(),
109 | usage: undefined
110 | };
111 | }
112 | }
113 |
114 | /**
115 | * Create a prompt string for Grok CLI from messages
116 | * @param messages - AI SDK message array
117 | * @returns Formatted prompt string
118 | */
119 | export function createPromptFromMessages(messages: AISDKMessage[]): string {
120 | const grokMessages = convertToGrokCliMessages(messages);
121 |
122 | // Create a conversation-style prompt
123 | const prompt = grokMessages
124 | .map((message) => {
125 | switch (message.role) {
126 | case 'system':
127 | return `System: ${message.content}`;
128 | case 'user':
129 | return `User: ${message.content}`;
130 | case 'assistant':
131 | return `Assistant: ${message.content}`;
132 | default:
133 | return `${message.role}: ${message.content}`;
134 | }
135 | })
136 | .join('\n\n');
137 |
138 | return prompt;
139 | }
140 |
141 | /**
142 | * Escape shell arguments for safe CLI execution
143 | * @param arg - Argument to escape
144 | * @returns Shell-escaped argument
145 | */
146 | export function escapeShellArg(arg: string | unknown): string {
147 | if (typeof arg !== 'string') {
148 | arg = String(arg);
149 | }
150 |
151 | // Replace single quotes with '\''
152 | return "'" + (arg as string).replace(/'/g, "'\\''") + "'";
153 | }
154 |
```
--------------------------------------------------------------------------------
/apps/cli/src/ui/display/tables.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Table display utilities
3 | * Provides table creation and formatting for tasks
4 | */
5 |
6 | import type { Subtask, Task, TaskPriority } from '@tm/core';
7 | import chalk from 'chalk';
8 | import Table from 'cli-table3';
9 | import { getComplexityWithColor } from '../formatters/complexity-formatters.js';
10 | import { getPriorityWithColor } from '../formatters/priority-formatters.js';
11 | import { getStatusWithColor } from '../formatters/status-formatters.js';
12 | import { getBoxWidth, truncate } from '../layout/helpers.js';
13 |
14 | /**
15 | * Default priority for tasks/subtasks when not specified
16 | */
17 | const DEFAULT_PRIORITY: TaskPriority = 'medium';
18 |
19 | /**
20 | * Create a task table for display
21 | */
22 | export function createTaskTable(
23 | tasks: (Task | Subtask)[],
24 | options?: {
25 | showSubtasks?: boolean;
26 | showComplexity?: boolean;
27 | showDependencies?: boolean;
28 | }
29 | ): string {
30 | const {
31 | showSubtasks = false,
32 | showComplexity = false,
33 | showDependencies = true
34 | } = options || {};
35 |
36 | // Calculate dynamic column widths based on terminal width
37 | const tableWidth = getBoxWidth(0.9, 100);
38 | // Adjust column widths to better match the original layout
39 | const baseColWidths = showComplexity
40 | ? [
41 | Math.floor(tableWidth * 0.1),
42 | Math.floor(tableWidth * 0.4),
43 | Math.floor(tableWidth * 0.15),
44 | Math.floor(tableWidth * 0.1),
45 | Math.floor(tableWidth * 0.2),
46 | Math.floor(tableWidth * 0.1)
47 | ] // ID, Title, Status, Priority, Dependencies, Complexity
48 | : [
49 | Math.floor(tableWidth * 0.08),
50 | Math.floor(tableWidth * 0.4),
51 | Math.floor(tableWidth * 0.18),
52 | Math.floor(tableWidth * 0.12),
53 | Math.floor(tableWidth * 0.2)
54 | ]; // ID, Title, Status, Priority, Dependencies
55 |
56 | const headers = [
57 | chalk.blue.bold('ID'),
58 | chalk.blue.bold('Title'),
59 | chalk.blue.bold('Status'),
60 | chalk.blue.bold('Priority')
61 | ];
62 | const colWidths = baseColWidths.slice(0, 4);
63 |
64 | if (showDependencies) {
65 | headers.push(chalk.blue.bold('Dependencies'));
66 | colWidths.push(baseColWidths[4]);
67 | }
68 |
69 | if (showComplexity) {
70 | headers.push(chalk.blue.bold('Complexity'));
71 | colWidths.push(baseColWidths[5] || 12);
72 | }
73 |
74 | const table = new Table({
75 | head: headers,
76 | style: { head: [], border: [] },
77 | colWidths,
78 | wordWrap: true
79 | });
80 |
81 | tasks.forEach((task) => {
82 | const row: string[] = [
83 | chalk.cyan(task.id.toString()),
84 | truncate(task.title, colWidths[1] - 3),
85 | getStatusWithColor(task.status, true), // Use table version
86 | getPriorityWithColor(task.priority)
87 | ];
88 |
89 | if (showDependencies) {
90 | // For table display, show simple format without status icons
91 | if (!task.dependencies || task.dependencies.length === 0) {
92 | row.push(chalk.gray('None'));
93 | } else {
94 | row.push(
95 | chalk.cyan(task.dependencies.map((d) => String(d)).join(', '))
96 | );
97 | }
98 | }
99 |
100 | if (showComplexity) {
101 | // Show complexity score from report if available
102 | if (typeof task.complexity === 'number') {
103 | row.push(getComplexityWithColor(task.complexity));
104 | } else {
105 | row.push(chalk.gray('N/A'));
106 | }
107 | }
108 |
109 | table.push(row);
110 |
111 | // Add subtasks if requested
112 | if (showSubtasks && task.subtasks && task.subtasks.length > 0) {
113 | task.subtasks.forEach((subtask) => {
114 | const subRow: string[] = [
115 | chalk.gray(` └─ ${subtask.id}`),
116 | chalk.gray(truncate(subtask.title, colWidths[1] - 6)),
117 | chalk.gray(getStatusWithColor(subtask.status, true)),
118 | chalk.gray(subtask.priority || DEFAULT_PRIORITY)
119 | ];
120 |
121 | if (showDependencies) {
122 | subRow.push(
123 | chalk.gray(
124 | subtask.dependencies && subtask.dependencies.length > 0
125 | ? subtask.dependencies.map((dep) => String(dep)).join(', ')
126 | : 'None'
127 | )
128 | );
129 | }
130 |
131 | if (showComplexity) {
132 | const complexityDisplay =
133 | typeof subtask.complexity === 'number'
134 | ? getComplexityWithColor(subtask.complexity)
135 | : '--';
136 | subRow.push(chalk.gray(complexityDisplay));
137 | }
138 |
139 | table.push(subRow);
140 | });
141 | }
142 | });
143 |
144 | return table.toString();
145 | }
146 |
```
--------------------------------------------------------------------------------
/scripts/modules/task-manager/remove-subtask.js:
--------------------------------------------------------------------------------
```javascript
1 | import { log, readJSON, writeJSON } from '../utils.js';
2 |
3 | /**
4 | * Remove a subtask from its parent task
5 | * @param {string} tasksPath - Path to the tasks.json file
6 | * @param {string} subtaskId - ID of the subtask to remove in format "parentId.subtaskId"
7 | * @param {boolean} convertToTask - Whether to convert the subtask to a standalone task
8 | * @param {boolean} generateFiles - Whether to regenerate task files after removing the subtask
9 | * @param {Object} context - Context object containing projectRoot and tag information
10 | * @param {string} [context.projectRoot] - Project root path
11 | * @param {string} [context.tag] - Tag for the task
12 | * @returns {Object|null} The removed subtask if convertToTask is true, otherwise null
13 | */
14 | async function removeSubtask(
15 | tasksPath,
16 | subtaskId,
17 | convertToTask = false,
18 | generateFiles = false,
19 | context = {}
20 | ) {
21 | const { projectRoot, tag } = context;
22 | try {
23 | log('info', `Removing subtask ${subtaskId}...`);
24 |
25 | // Read the existing tasks with proper context
26 | const data = readJSON(tasksPath, projectRoot, tag);
27 | if (!data || !data.tasks) {
28 | throw new Error(`Invalid or missing tasks file at ${tasksPath}`);
29 | }
30 |
31 | // Parse the subtask ID (format: "parentId.subtaskId")
32 | if (!subtaskId.includes('.')) {
33 | throw new Error(
34 | `Invalid subtask ID format: ${subtaskId}. Expected format: "parentId.subtaskId"`
35 | );
36 | }
37 |
38 | const [parentIdStr, subtaskIdStr] = subtaskId.split('.');
39 | const parentId = parseInt(parentIdStr, 10);
40 | const subtaskIdNum = parseInt(subtaskIdStr, 10);
41 |
42 | // Find the parent task
43 | const parentTask = data.tasks.find((t) => t.id === parentId);
44 | if (!parentTask) {
45 | throw new Error(`Parent task with ID ${parentId} not found`);
46 | }
47 |
48 | // Check if parent has subtasks
49 | if (!parentTask.subtasks || parentTask.subtasks.length === 0) {
50 | throw new Error(`Parent task ${parentId} has no subtasks`);
51 | }
52 |
53 | // Find the subtask to remove
54 | const subtaskIndex = parentTask.subtasks.findIndex(
55 | (st) => st.id === subtaskIdNum
56 | );
57 | if (subtaskIndex === -1) {
58 | throw new Error(`Subtask ${subtaskId} not found`);
59 | }
60 |
61 | // Get a copy of the subtask before removing it
62 | const removedSubtask = { ...parentTask.subtasks[subtaskIndex] };
63 |
64 | // Remove the subtask from the parent
65 | parentTask.subtasks.splice(subtaskIndex, 1);
66 |
67 | // If parent has no more subtasks, remove the subtasks array
68 | if (parentTask.subtasks.length === 0) {
69 | parentTask.subtasks = undefined;
70 | }
71 |
72 | let convertedTask = null;
73 |
74 | // Convert the subtask to a standalone task if requested
75 | if (convertToTask) {
76 | log('info', `Converting subtask ${subtaskId} to a standalone task...`);
77 |
78 | // Find the highest task ID to determine the next ID
79 | const highestId = Math.max(...data.tasks.map((t) => t.id));
80 | const newTaskId = highestId + 1;
81 |
82 | // Create the new task from the subtask
83 | convertedTask = {
84 | id: newTaskId,
85 | title: removedSubtask.title,
86 | description: removedSubtask.description || '',
87 | details: removedSubtask.details || '',
88 | status: removedSubtask.status || 'pending',
89 | dependencies: removedSubtask.dependencies || [],
90 | priority: parentTask.priority || 'medium' // Inherit priority from parent
91 | };
92 |
93 | // Add the parent task as a dependency if not already present
94 | if (!convertedTask.dependencies.includes(parentId)) {
95 | convertedTask.dependencies.push(parentId);
96 | }
97 |
98 | // Add the converted task to the tasks array
99 | data.tasks.push(convertedTask);
100 |
101 | log('info', `Created new task ${newTaskId} from subtask ${subtaskId}`);
102 | } else {
103 | log('info', `Subtask ${subtaskId} deleted`);
104 | }
105 |
106 | // Write the updated tasks back to the file with proper context
107 | writeJSON(tasksPath, data, projectRoot, tag);
108 |
109 | // Note: Task file generation is no longer supported and has been removed
110 |
111 | return convertedTask;
112 | } catch (error) {
113 | log('error', `Error removing subtask: ${error.message}`);
114 | throw error;
115 | }
116 | }
117 |
118 | export default removeSubtask;
119 |
```
--------------------------------------------------------------------------------
/tests/integration/profiles/roo-files-inclusion.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 | import { execSync } from 'child_process';
6 |
7 | describe('Roo Files Inclusion in Package', () => {
8 | // This test verifies that the required Roo files are included in the final package
9 |
10 | test('package.json includes dist/** in the "files" array for bundled files', () => {
11 | // Read the package.json file
12 | const packageJsonPath = path.join(process.cwd(), 'package.json');
13 | const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
14 |
15 | // Check if dist/** is included in the files array (which contains bundled output including Roo files)
16 | expect(packageJson.files).toContain('dist/**');
17 | });
18 |
19 | test('roo.js profile contains logic for Roo directory creation and file copying', () => {
20 | // Read the roo.js profile file
21 | const rooJsPath = path.join(process.cwd(), 'src', 'profiles', 'roo.js');
22 | const rooJsContent = fs.readFileSync(rooJsPath, 'utf8');
23 |
24 | // Check for the main handler function
25 | expect(
26 | rooJsContent.includes('onAddRulesProfile(targetDir, assetsDir)')
27 | ).toBe(true);
28 |
29 | // Check for general recursive copy of assets/roocode
30 | expect(
31 | rooJsContent.includes('copyRecursiveSync(sourceDir, targetDir)')
32 | ).toBe(true);
33 |
34 | // Check for updated path handling
35 | expect(rooJsContent.includes("path.join(assetsDir, 'roocode')")).toBe(true);
36 |
37 | // Check for .roomodes file copying logic (source and destination paths)
38 | expect(rooJsContent.includes("path.join(sourceDir, '.roomodes')")).toBe(
39 | true
40 | );
41 | expect(rooJsContent.includes("path.join(targetDir, '.roomodes')")).toBe(
42 | true
43 | );
44 |
45 | // Check for mode-specific rule file copying logic
46 | expect(rooJsContent.includes('for (const mode of ROO_MODES)')).toBe(true);
47 | expect(
48 | rooJsContent.includes(
49 | 'path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`)'
50 | )
51 | ).toBe(true);
52 | expect(
53 | rooJsContent.includes(
54 | "path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`)"
55 | )
56 | ).toBe(true);
57 |
58 | // Check for import of ROO_MODES from profiles.js instead of local definition
59 | expect(
60 | rooJsContent.includes(
61 | "import { ROO_MODES } from '../constants/profiles.js'"
62 | )
63 | ).toBe(true);
64 |
65 | // Verify ROO_MODES is used in the for loop
66 | expect(rooJsContent.includes('for (const mode of ROO_MODES)')).toBe(true);
67 |
68 | // Verify mode variable is used in the template strings (this confirms modes are being processed)
69 | expect(rooJsContent.includes('rules-${mode}')).toBe(true);
70 | expect(rooJsContent.includes('${mode}-rules')).toBe(true);
71 |
72 | // Verify that the ROO_MODES constant is properly imported and used
73 | // We should be able to find the template literals that use the mode variable
74 | expect(rooJsContent.includes('`rules-${mode}`')).toBe(true);
75 | expect(rooJsContent.includes('`${mode}-rules`')).toBe(true);
76 | expect(rooJsContent.includes('Copied ${mode}-rules to ${dest}')).toBe(true);
77 |
78 | // Also verify that the expected mode names are defined in the imported constant
79 | // by checking that the import is from the correct file that contains all 6 modes
80 | const profilesConstantsPath = path.join(
81 | process.cwd(),
82 | 'src',
83 | 'constants',
84 | 'profiles.js'
85 | );
86 | const profilesContent = fs.readFileSync(profilesConstantsPath, 'utf8');
87 |
88 | // Check that ROO_MODES is exported and contains all expected modes
89 | expect(profilesContent.includes('export const ROO_MODES')).toBe(true);
90 | const expectedModes = [
91 | 'architect',
92 | 'ask',
93 | 'orchestrator',
94 | 'code',
95 | 'debug',
96 | 'test'
97 | ];
98 | expectedModes.forEach((mode) => {
99 | expect(profilesContent.includes(`'${mode}'`)).toBe(true);
100 | });
101 | });
102 |
103 | test('source Roo files exist in assets directory', () => {
104 | // Verify that the source files for Roo integration exist
105 | expect(
106 | fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roo'))
107 | ).toBe(true);
108 | expect(
109 | fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roomodes'))
110 | ).toBe(true);
111 | });
112 | });
113 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/auth/services/context-store.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Context storage for app-specific user preferences
3 | *
4 | * This store manages user preferences and context separate from auth tokens.
5 | * - selectedContext (org/brief selection)
6 | * - userId and email (for convenience)
7 | * - Any other app-specific data
8 | *
9 | * Stored at: ~/.taskmaster/context.json
10 | */
11 |
12 | import fs from 'fs';
13 | import path from 'path';
14 | import { getLogger } from '../../../common/logger/index.js';
15 | import { AuthenticationError, UserContext } from '../types.js';
16 |
17 | const DEFAULT_CONTEXT_FILE = path.join(
18 | process.env.HOME || process.env.USERPROFILE || '~',
19 | '.taskmaster',
20 | 'context.json'
21 | );
22 |
23 | export interface StoredContext {
24 | userId?: string;
25 | email?: string;
26 | selectedContext?: UserContext;
27 | lastUpdated: string;
28 | }
29 |
30 | export class ContextStore {
31 | private static instance: ContextStore | null = null;
32 | private logger = getLogger('ContextStore');
33 | private contextPath: string;
34 |
35 | private constructor(contextPath: string = DEFAULT_CONTEXT_FILE) {
36 | this.contextPath = contextPath;
37 | }
38 |
39 | /**
40 | * Get singleton instance
41 | */
42 | static getInstance(contextPath?: string): ContextStore {
43 | if (!ContextStore.instance) {
44 | ContextStore.instance = new ContextStore(contextPath);
45 | }
46 | return ContextStore.instance;
47 | }
48 |
49 | /**
50 | * Reset singleton (for testing)
51 | */
52 | static resetInstance(): void {
53 | ContextStore.instance = null;
54 | }
55 |
56 | /**
57 | * Get stored context
58 | */
59 | getContext(): StoredContext | null {
60 | try {
61 | if (!fs.existsSync(this.contextPath)) {
62 | return null;
63 | }
64 |
65 | const data = JSON.parse(fs.readFileSync(this.contextPath, 'utf8'));
66 | this.logger.debug('Loaded context from disk');
67 | return data;
68 | } catch (error) {
69 | this.logger.error('Failed to read context:', error);
70 | return null;
71 | }
72 | }
73 |
74 | /**
75 | * Save context
76 | */
77 | saveContext(context: Partial<StoredContext>): void {
78 | try {
79 | // Load existing context
80 | const existing = this.getContext() || {};
81 |
82 | // Merge with new data
83 | const updated: StoredContext = {
84 | ...existing,
85 | ...context,
86 | lastUpdated: new Date().toISOString()
87 | };
88 |
89 | // Ensure directory exists
90 | const dir = path.dirname(this.contextPath);
91 | if (!fs.existsSync(dir)) {
92 | fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
93 | }
94 |
95 | // Write atomically
96 | const tempFile = `${this.contextPath}.tmp`;
97 | fs.writeFileSync(tempFile, JSON.stringify(updated, null, 2), {
98 | mode: 0o600
99 | });
100 | fs.renameSync(tempFile, this.contextPath);
101 |
102 | this.logger.debug('Saved context to disk');
103 | } catch (error) {
104 | throw new AuthenticationError(
105 | `Failed to save context: ${(error as Error).message}`,
106 | 'SAVE_FAILED',
107 | error
108 | );
109 | }
110 | }
111 |
112 | /**
113 | * Update user context (org/brief selection)
114 | */
115 | updateUserContext(userContext: Partial<UserContext>): void {
116 | const existing = this.getContext();
117 | const currentUserContext = existing?.selectedContext || {};
118 |
119 | const updated: UserContext = {
120 | ...currentUserContext,
121 | ...userContext,
122 | updatedAt: new Date().toISOString()
123 | };
124 |
125 | this.saveContext({
126 | ...existing,
127 | selectedContext: updated
128 | });
129 | }
130 |
131 | /**
132 | * Get user context (org/brief selection)
133 | */
134 | getUserContext(): UserContext | null {
135 | const context = this.getContext();
136 | return context?.selectedContext || null;
137 | }
138 |
139 | /**
140 | * Clear user context
141 | */
142 | clearUserContext(): void {
143 | const existing = this.getContext();
144 | if (existing) {
145 | const { selectedContext, ...rest } = existing;
146 | this.saveContext(rest);
147 | }
148 | }
149 |
150 | /**
151 | * Clear all context
152 | */
153 | clearContext(): void {
154 | try {
155 | if (fs.existsSync(this.contextPath)) {
156 | fs.unlinkSync(this.contextPath);
157 | this.logger.debug('Cleared context from disk');
158 | }
159 | } catch (error) {
160 | throw new AuthenticationError(
161 | `Failed to clear context: ${(error as Error).message}`,
162 | 'CLEAR_FAILED',
163 | error
164 | );
165 | }
166 | }
167 |
168 | /**
169 | * Check if context exists
170 | */
171 | hasContext(): boolean {
172 | return this.getContext() !== null;
173 | }
174 |
175 | /**
176 | * Get context file path
177 | */
178 | getContextPath(): string {
179 | return this.contextPath;
180 | }
181 | }
182 |
```
--------------------------------------------------------------------------------
/mcp-server/src/index.js:
--------------------------------------------------------------------------------
```javascript
1 | import { FastMCP } from 'fastmcp';
2 | import path from 'path';
3 | import dotenv from 'dotenv';
4 | import { fileURLToPath } from 'url';
5 | import fs from 'fs';
6 | import logger from './logger.js';
7 | import {
8 | registerTaskMasterTools,
9 | getToolsConfiguration
10 | } from './tools/index.js';
11 | import ProviderRegistry from '../../src/provider-registry/index.js';
12 | import { MCPProvider } from './providers/mcp-provider.js';
13 | import packageJson from '../../package.json' with { type: 'json' };
14 |
15 | dotenv.config();
16 |
17 | // Constants
18 | const __filename = fileURLToPath(import.meta.url);
19 | const __dirname = path.dirname(__filename);
20 |
21 | /**
22 | * Main MCP server class that integrates with Task Master
23 | */
24 | class TaskMasterMCPServer {
25 | constructor() {
26 | this.options = {
27 | name: 'Task Master MCP Server',
28 | version: packageJson.version
29 | };
30 |
31 | this.server = new FastMCP(this.options);
32 | this.initialized = false;
33 |
34 | this.init = this.init.bind(this);
35 | this.start = this.start.bind(this);
36 | this.stop = this.stop.bind(this);
37 |
38 | this.logger = logger;
39 | }
40 |
41 | /**
42 | * Initialize the MCP server with necessary tools and routes
43 | */
44 | async init() {
45 | if (this.initialized) return;
46 |
47 | const normalizedToolMode = getToolsConfiguration();
48 |
49 | this.logger.info('Task Master MCP Server starting...');
50 | this.logger.info(`Tool mode configuration: ${normalizedToolMode}`);
51 |
52 | const registrationResult = registerTaskMasterTools(
53 | this.server,
54 | normalizedToolMode
55 | );
56 |
57 | this.logger.info(
58 | `Normalized tool mode: ${registrationResult.normalizedMode}`
59 | );
60 | this.logger.info(
61 | `Registered ${registrationResult.registeredTools.length} tools successfully`
62 | );
63 |
64 | if (registrationResult.registeredTools.length > 0) {
65 | this.logger.debug(
66 | `Registered tools: ${registrationResult.registeredTools.join(', ')}`
67 | );
68 | }
69 |
70 | if (registrationResult.failedTools.length > 0) {
71 | this.logger.warn(
72 | `Failed to register ${registrationResult.failedTools.length} tools: ${registrationResult.failedTools.join(', ')}`
73 | );
74 | }
75 |
76 | this.initialized = true;
77 |
78 | return this;
79 | }
80 |
81 | /**
82 | * Start the MCP server
83 | */
84 | async start() {
85 | if (!this.initialized) {
86 | await this.init();
87 | }
88 |
89 | this.server.on('connect', (event) => {
90 | event.session.server.sendLoggingMessage({
91 | data: {
92 | context: event.session.context,
93 | message: `MCP Server connected: ${event.session.name}`
94 | },
95 | level: 'info'
96 | });
97 | this.registerRemoteProvider(event.session);
98 | });
99 |
100 | // Start the FastMCP server with increased timeout
101 | await this.server.start({
102 | transportType: 'stdio',
103 | timeout: 120000 // 2 minutes timeout (in milliseconds)
104 | });
105 |
106 | return this;
107 | }
108 |
109 | /**
110 | * Register both MCP providers with the provider registry
111 | */
112 | registerRemoteProvider(session) {
113 | // Check if the server has at least one session
114 | if (session) {
115 | // Make sure session has required capabilities
116 | if (!session.clientCapabilities || !session.clientCapabilities.sampling) {
117 | session.server.sendLoggingMessage({
118 | data: {
119 | context: session.context,
120 | message: `MCP session missing required sampling capabilities, providers not registered`
121 | },
122 | level: 'info'
123 | });
124 | return;
125 | }
126 |
127 | // Register MCP provider with the Provider Registry
128 |
129 | // Register the unified MCP provider
130 | const mcpProvider = new MCPProvider();
131 | mcpProvider.setSession(session);
132 |
133 | // Register provider with the registry
134 | const providerRegistry = ProviderRegistry.getInstance();
135 | providerRegistry.registerProvider('mcp', mcpProvider);
136 |
137 | session.server.sendLoggingMessage({
138 | data: {
139 | context: session.context,
140 | message: `MCP Server connected`
141 | },
142 | level: 'info'
143 | });
144 | } else {
145 | session.server.sendLoggingMessage({
146 | data: {
147 | context: session.context,
148 | message: `No MCP sessions available, providers not registered`
149 | },
150 | level: 'warn'
151 | });
152 | }
153 | }
154 |
155 | /**
156 | * Stop the MCP server
157 | */
158 | async stop() {
159 | if (this.server) {
160 | await this.server.stop();
161 | }
162 | }
163 | }
164 |
165 | export default TaskMasterMCPServer;
166 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/utils/run-id-generator.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Run ID generation and validation utilities for the global storage system.
3 | * Uses ISO 8601 timestamps with millisecond precision for unique, chronologically-ordered run IDs.
4 | *
5 | * @module run-id-generator
6 | */
7 |
8 | // Collision detection state
9 | let lastTimestamp = 0;
10 | let counter = 0;
11 |
12 | /**
13 | * Generates a unique run ID using ISO 8601 timestamp format with millisecond precision.
14 | * The ID is guaranteed to be chronologically sortable and URL-safe.
15 | * Includes collision detection to ensure uniqueness even when called in rapid succession.
16 | *
17 | * @param {Date} [date=new Date()] - Optional date to use for the run ID. Defaults to current time.
18 | * @returns {string} ISO 8601 formatted timestamp (e.g., '2024-01-15T10:30:45.123Z')
19 | *
20 | * @example
21 | * generateRunId() // returns '2024-01-15T10:30:45.123Z'
22 | * generateRunId(new Date('2024-01-15T10:00:00.000Z')) // returns '2024-01-15T10:00:00.000Z'
23 | */
24 | export function generateRunId(date: Date = new Date()): string {
25 | const timestamp = date.getTime();
26 |
27 | // Collision detection: if same millisecond, wait for next millisecond
28 | if (timestamp === lastTimestamp) {
29 | counter++;
30 | // Wait for next millisecond to ensure uniqueness
31 | let newTimestamp = timestamp;
32 | while (newTimestamp === timestamp) {
33 | newTimestamp = Date.now();
34 | }
35 | date = new Date(newTimestamp);
36 | lastTimestamp = newTimestamp;
37 | counter = 0;
38 | } else {
39 | lastTimestamp = timestamp;
40 | counter = 0;
41 | }
42 |
43 | return date.toISOString();
44 | }
45 |
46 | /**
47 | * Validates whether a string is a valid run ID.
48 | * A valid run ID must be:
49 | * - In ISO 8601 format with milliseconds
50 | * - In UTC timezone (ends with 'Z')
51 | * - A valid date when parsed
52 | *
53 | * @param {any} runId - The value to validate
54 | * @returns {boolean} True if the value is a valid run ID
55 | *
56 | * @example
57 | * isValidRunId('2024-01-15T10:30:45.123Z') // returns true
58 | * isValidRunId('invalid') // returns false
59 | * isValidRunId('2024-01-15T10:30:45Z') // returns false (missing milliseconds)
60 | */
61 | export function isValidRunId(runId: any): boolean {
62 | if (!runId || typeof runId !== 'string') {
63 | return false;
64 | }
65 |
66 | // Check format: YYYY-MM-DDTHH:mm:ss.sssZ
67 | const isoFormatRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
68 | if (!isoFormatRegex.test(runId)) {
69 | return false;
70 | }
71 |
72 | // Validate it's a real date
73 | const date = new Date(runId);
74 | if (isNaN(date.getTime())) {
75 | return false;
76 | }
77 |
78 | // Ensure the parsed date matches the input (catches invalid dates like 2024-13-01)
79 | return date.toISOString() === runId;
80 | }
81 |
82 | /**
83 | * Parses a run ID string into a Date object.
84 | *
85 | * @param {any} runId - The run ID to parse
86 | * @returns {Date | null} Date object if valid, null if invalid
87 | *
88 | * @example
89 | * parseRunId('2024-01-15T10:30:45.123Z') // returns Date object
90 | * parseRunId('invalid') // returns null
91 | */
92 | export function parseRunId(runId: any): Date | null {
93 | if (!isValidRunId(runId)) {
94 | return null;
95 | }
96 |
97 | return new Date(runId);
98 | }
99 |
100 | /**
101 | * Compares two run IDs chronologically.
102 | * Returns a negative number if id1 is earlier, positive if id1 is later, or 0 if equal.
103 | * Can be used as a comparator function for Array.sort().
104 | *
105 | * @param {string} id1 - First run ID to compare
106 | * @param {string} id2 - Second run ID to compare
107 | * @returns {number} Negative if id1 < id2, positive if id1 > id2, zero if equal
108 | * @throws {Error} If either run ID is invalid
109 | *
110 | * @example
111 | * compareRunIds('2024-01-15T10:00:00.000Z', '2024-01-15T11:00:00.000Z') // returns negative number
112 | * ['2024-01-15T14:00:00.000Z', '2024-01-15T10:00:00.000Z'].sort(compareRunIds)
113 | * // returns ['2024-01-15T10:00:00.000Z', '2024-01-15T14:00:00.000Z']
114 | */
115 | export function compareRunIds(id1: string, id2: string): number {
116 | if (!isValidRunId(id1)) {
117 | throw new Error(`Invalid run ID: ${id1}`);
118 | }
119 |
120 | if (!isValidRunId(id2)) {
121 | throw new Error(`Invalid run ID: ${id2}`);
122 | }
123 |
124 | // String comparison works for ISO 8601 timestamps
125 | // because they are lexicographically sortable
126 | if (id1 < id2) return -1;
127 | if (id1 > id2) return 1;
128 | return 0;
129 | }
130 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/remove-task.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // --- Mock dependencies BEFORE module import ---
4 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
5 | readJSON: jest.fn(),
6 | writeJSON: jest.fn(),
7 | log: jest.fn(),
8 | CONFIG: {
9 | model: 'mock-model',
10 | maxTokens: 4000,
11 | temperature: 0.7,
12 | debug: false
13 | },
14 | findTaskById: jest.fn(),
15 | truncate: jest.fn((t) => t),
16 | isSilentMode: jest.fn(() => false)
17 | }));
18 |
19 | jest.unstable_mockModule(
20 | '../../../../../scripts/modules/task-manager/generate-task-files.js',
21 | () => ({
22 | default: jest.fn().mockResolvedValue()
23 | })
24 | );
25 |
26 | // fs is used for file deletion side-effects – stub the methods we touch
27 | jest.unstable_mockModule('fs', () => ({
28 | existsSync: jest.fn(() => true),
29 | unlinkSync: jest.fn()
30 | }));
31 |
32 | // path is fine to keep as real since only join/dirname used – no side effects
33 |
34 | // Import mocked modules
35 | const { readJSON, writeJSON, log } = await import(
36 | '../../../../../scripts/modules/utils.js'
37 | );
38 | const generateTaskFiles = (
39 | await import(
40 | '../../../../../scripts/modules/task-manager/generate-task-files.js'
41 | )
42 | ).default;
43 | const fs = await import('fs');
44 |
45 | // Import module under test (AFTER mocks in place)
46 | const { default: removeTask } = await import(
47 | '../../../../../scripts/modules/task-manager/remove-task.js'
48 | );
49 |
50 | // ---- Test data helpers ----
51 | const buildSampleTaggedTasks = () => ({
52 | master: {
53 | tasks: [
54 | { id: 1, title: 'Task 1', status: 'pending', dependencies: [] },
55 | { id: 2, title: 'Task 2', status: 'pending', dependencies: [1] },
56 | {
57 | id: 3,
58 | title: 'Parent',
59 | status: 'pending',
60 | dependencies: [],
61 | subtasks: [
62 | { id: 1, title: 'Sub 3.1', status: 'pending', dependencies: [] }
63 | ]
64 | }
65 | ]
66 | },
67 | other: {
68 | tasks: [{ id: 99, title: 'Shadow', status: 'pending', dependencies: [1] }]
69 | }
70 | });
71 |
72 | // Utility to deep clone sample each test
73 | const getFreshData = () => JSON.parse(JSON.stringify(buildSampleTaggedTasks()));
74 |
75 | // ----- Tests -----
76 |
77 | describe('removeTask', () => {
78 | beforeEach(() => {
79 | jest.clearAllMocks();
80 | // readJSON returns deep copy so each test isolated
81 | readJSON.mockImplementation(() => {
82 | return {
83 | ...getFreshData().master,
84 | tag: 'master',
85 | _rawTaggedData: getFreshData()
86 | };
87 | });
88 | writeJSON.mockResolvedValue();
89 | log.mockImplementation(() => {});
90 | fs.unlinkSync.mockImplementation(() => {});
91 | });
92 |
93 | test('removes a main task and cleans dependencies across tags', async () => {
94 | const result = await removeTask('tasks/tasks.json', '1', { tag: 'master' });
95 |
96 | // Expect success true
97 | expect(result.success).toBe(true);
98 | // writeJSON called with data where task 1 is gone in master & dependencies removed in other tags
99 | const written = writeJSON.mock.calls[0][1];
100 | expect(written.master.tasks.find((t) => t.id === 1)).toBeUndefined();
101 | // deps removed from child tasks
102 | const task2 = written.master.tasks.find((t) => t.id === 2);
103 | expect(task2.dependencies).not.toContain(1);
104 | const shadow = written.other.tasks.find((t) => t.id === 99);
105 | expect(shadow.dependencies).not.toContain(1);
106 | // Task file deletion attempted
107 | expect(fs.unlinkSync).toHaveBeenCalled();
108 | });
109 |
110 | test('removes a subtask only and leaves parent intact', async () => {
111 | const result = await removeTask('tasks/tasks.json', '3.1', {
112 | tag: 'master'
113 | });
114 |
115 | expect(result.success).toBe(true);
116 | const written = writeJSON.mock.calls[0][1];
117 | const parent = written.master.tasks.find((t) => t.id === 3);
118 | expect(parent.subtasks || []).toHaveLength(0);
119 | // Ensure parent still exists
120 | expect(parent).toBeDefined();
121 | // No task files should be deleted for subtasks
122 | expect(fs.unlinkSync).not.toHaveBeenCalled();
123 | });
124 |
125 | test('handles non-existent task gracefully', async () => {
126 | const result = await removeTask('tasks/tasks.json', '42', {
127 | tag: 'master'
128 | });
129 | expect(result.success).toBe(false);
130 | expect(result.error).toContain('not found');
131 | // writeJSON not called because nothing changed
132 | expect(writeJSON).not.toHaveBeenCalled();
133 | });
134 | });
135 |
```
--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/expand-all-tasks.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Direct function wrapper for expandAllTasks
3 | */
4 |
5 | import { expandAllTasks } from '../../../../scripts/modules/task-manager.js';
6 | import {
7 | enableSilentMode,
8 | disableSilentMode
9 | } from '../../../../scripts/modules/utils.js';
10 | import { createLogWrapper } from '../../tools/utils.js';
11 | import { resolveComplexityReportOutputPath } from '../../../../src/utils/path-utils.js';
12 |
13 | /**
14 | * Expand all pending tasks with subtasks (Direct Function Wrapper)
15 | * @param {Object} args - Function arguments
16 | * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
17 | * @param {number|string} [args.num] - Number of subtasks to generate
18 | * @param {boolean} [args.research] - Enable research-backed subtask generation
19 | * @param {string} [args.prompt] - Additional context to guide subtask generation
20 | * @param {boolean} [args.force] - Force regeneration of subtasks for tasks that already have them
21 | * @param {string} [args.projectRoot] - Project root path.
22 | * @param {string} [args.tag] - Tag for the task (optional)
23 | * @param {Object} log - Logger object from FastMCP
24 | * @param {Object} context - Context object containing session
25 | * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
26 | */
27 | export async function expandAllTasksDirect(args, log, context = {}) {
28 | const { session } = context; // Extract session
29 | // Destructure expected args, including projectRoot and complexityReportPath
30 | const {
31 | tasksJsonPath,
32 | num,
33 | research,
34 | prompt,
35 | force,
36 | projectRoot,
37 | tag,
38 | complexityReportPath: providedComplexityReportPath
39 | } = args;
40 |
41 | // Create logger wrapper using the utility
42 | const mcpLog = createLogWrapper(log);
43 |
44 | // Use provided complexity report path or compute it
45 | const complexityReportPath =
46 | providedComplexityReportPath ||
47 | resolveComplexityReportOutputPath(null, { projectRoot, tag }, log);
48 |
49 | log.info(
50 | `Expand all tasks will use complexity report at: ${complexityReportPath}`
51 | );
52 |
53 | if (!tasksJsonPath) {
54 | log.error('expandAllTasksDirect called without tasksJsonPath');
55 | return {
56 | success: false,
57 | error: {
58 | code: 'MISSING_ARGUMENT',
59 | message: 'tasksJsonPath is required'
60 | }
61 | };
62 | }
63 |
64 | enableSilentMode(); // Enable silent mode for the core function call
65 | try {
66 | log.info(
67 | `Calling core expandAllTasks with args: ${JSON.stringify({ num, research, prompt, force, projectRoot, tag })}`
68 | );
69 |
70 | // Parse parameters (ensure correct types)
71 | const numSubtasks = num ? parseInt(num, 10) : undefined;
72 | const useResearch = research === true;
73 | const additionalContext = prompt || '';
74 | const forceFlag = force === true;
75 |
76 | // Call the core function, passing options and the context object { session, mcpLog, projectRoot, tag, complexityReportPath }
77 | const result = await expandAllTasks(
78 | tasksJsonPath,
79 | numSubtasks,
80 | useResearch,
81 | additionalContext,
82 | forceFlag,
83 | { session, mcpLog, projectRoot, tag, complexityReportPath },
84 | 'json'
85 | );
86 |
87 | // Core function now returns a summary object including the *aggregated* telemetryData
88 | return {
89 | success: true,
90 | data: {
91 | message: `Expand all operation completed. Expanded: ${result.expandedCount}, Failed: ${result.failedCount}, Skipped: ${result.skippedCount}`,
92 | details: {
93 | expandedCount: result.expandedCount,
94 | failedCount: result.failedCount,
95 | skippedCount: result.skippedCount,
96 | tasksToExpand: result.tasksToExpand
97 | },
98 | telemetryData: result.telemetryData // Pass the aggregated object
99 | }
100 | };
101 | } catch (error) {
102 | // Log the error using the MCP logger
103 | log.error(`Error during core expandAllTasks execution: ${error.message}`);
104 | // Optionally log stack trace if available and debug enabled
105 | // if (error.stack && log.debug) { log.debug(error.stack); }
106 |
107 | return {
108 | success: false,
109 | error: {
110 | code: 'CORE_FUNCTION_ERROR', // Or a more specific code if possible
111 | message: error.message
112 | }
113 | };
114 | } finally {
115 | disableSilentMode(); // IMPORTANT: Ensure silent mode is always disabled
116 | }
117 | }
118 |
```
--------------------------------------------------------------------------------
/tests/integration/claude-code-optional.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mock AI SDK functions at the top level
4 | const generateText = jest.fn();
5 | const streamText = jest.fn();
6 |
7 | jest.unstable_mockModule('ai', () => ({
8 | generateObject: jest.fn(),
9 | generateText,
10 | streamText,
11 | streamObject: jest.fn(),
12 | zodSchema: jest.fn(),
13 | JSONParseError: class JSONParseError extends Error {},
14 | NoObjectGeneratedError: class NoObjectGeneratedError extends Error {}
15 | }));
16 |
17 | // Mock successful provider creation for all tests
18 | const mockProvider = jest.fn((modelId) => ({
19 | id: modelId,
20 | doGenerate: jest.fn(),
21 | doStream: jest.fn()
22 | }));
23 | mockProvider.languageModel = jest.fn((id, settings) => ({ id, settings }));
24 | mockProvider.chat = mockProvider.languageModel;
25 |
26 | jest.unstable_mockModule('ai-sdk-provider-claude-code', () => ({
27 | createClaudeCode: jest.fn(() => mockProvider)
28 | }));
29 |
30 | // Import the provider after mocking
31 | const { ClaudeCodeProvider } = await import(
32 | '../../src/ai-providers/claude-code.js'
33 | );
34 |
35 | describe('Claude Code Integration (Optional)', () => {
36 | beforeEach(() => {
37 | jest.clearAllMocks();
38 | });
39 |
40 | it('should create a working provider instance', () => {
41 | const provider = new ClaudeCodeProvider();
42 | expect(provider.name).toBe('Claude Code');
43 | expect(provider.getSupportedModels()).toEqual(['opus', 'sonnet', 'haiku']);
44 | });
45 |
46 | it('should support model validation', () => {
47 | const provider = new ClaudeCodeProvider();
48 | expect(provider.isModelSupported('sonnet')).toBe(true);
49 | expect(provider.isModelSupported('opus')).toBe(true);
50 | expect(provider.isModelSupported('haiku')).toBe(true);
51 | expect(provider.isModelSupported('unknown')).toBe(false);
52 | });
53 |
54 | it('should create a client successfully', () => {
55 | const provider = new ClaudeCodeProvider();
56 | const client = provider.getClient();
57 |
58 | expect(client).toBeDefined();
59 | expect(typeof client).toBe('function');
60 | expect(client.languageModel).toBeDefined();
61 | expect(client.chat).toBeDefined();
62 | expect(client.chat).toBe(client.languageModel);
63 | });
64 |
65 | it('should pass command-specific settings to client', async () => {
66 | const provider = new ClaudeCodeProvider();
67 | const client = provider.getClient({ commandName: 'test-command' });
68 |
69 | expect(client).toBeDefined();
70 | expect(typeof client).toBe('function');
71 | const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
72 | expect(createClaudeCode).toHaveBeenCalledTimes(1);
73 | });
74 |
75 | it('should handle AI SDK generateText integration', async () => {
76 | const provider = new ClaudeCodeProvider();
77 | const client = provider.getClient();
78 |
79 | // Mock successful generation
80 | generateText.mockResolvedValueOnce({
81 | text: 'Hello from Claude Code!',
82 | usage: { totalTokens: 10 }
83 | });
84 |
85 | const result = await generateText({
86 | model: client('sonnet'),
87 | messages: [{ role: 'user', content: 'Hello' }]
88 | });
89 |
90 | expect(result.text).toBe('Hello from Claude Code!');
91 | expect(generateText).toHaveBeenCalledWith({
92 | model: expect.any(Object),
93 | messages: [{ role: 'user', content: 'Hello' }]
94 | });
95 | });
96 |
97 | it('should handle AI SDK streamText integration', async () => {
98 | const provider = new ClaudeCodeProvider();
99 | const client = provider.getClient();
100 |
101 | // Mock successful streaming
102 | const mockStream = {
103 | textStream: (async function* () {
104 | yield 'Streamed response';
105 | })()
106 | };
107 | streamText.mockResolvedValueOnce(mockStream);
108 |
109 | const streamResult = await streamText({
110 | model: client('sonnet'),
111 | messages: [{ role: 'user', content: 'Stream test' }]
112 | });
113 |
114 | expect(streamResult.textStream).toBeDefined();
115 | expect(streamText).toHaveBeenCalledWith({
116 | model: expect.any(Object),
117 | messages: [{ role: 'user', content: 'Stream test' }]
118 | });
119 | });
120 |
121 | it('should not require authentication validation', () => {
122 | const provider = new ClaudeCodeProvider();
123 | expect(provider.isRequiredApiKey()).toBe(false);
124 | expect(() => provider.validateAuth()).not.toThrow();
125 | expect(() => provider.validateAuth({})).not.toThrow();
126 | expect(() => provider.validateAuth({ commandName: 'test' })).not.toThrow();
127 | });
128 | });
129 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/services/config-loader.service.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Unit tests for ConfigLoader service
3 | */
4 |
5 | import fs from 'node:fs/promises';
6 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7 | import { DEFAULT_CONFIG_VALUES } from '../../../common/interfaces/configuration.interface.js';
8 | import { ConfigLoader } from './config-loader.service.js';
9 |
10 | vi.mock('node:fs', () => ({
11 | promises: {
12 | readFile: vi.fn(),
13 | access: vi.fn()
14 | }
15 | }));
16 |
17 | describe('ConfigLoader', () => {
18 | let configLoader: ConfigLoader;
19 | const testProjectRoot = '/test/project';
20 |
21 | beforeEach(() => {
22 | configLoader = new ConfigLoader(testProjectRoot);
23 | vi.clearAllMocks();
24 | });
25 |
26 | afterEach(() => {
27 | vi.restoreAllMocks();
28 | });
29 |
30 | describe('getDefaultConfig', () => {
31 | it('should return default configuration values', () => {
32 | const config = configLoader.getDefaultConfig();
33 |
34 | expect(config.models).toEqual({
35 | main: DEFAULT_CONFIG_VALUES.MODELS.MAIN,
36 | fallback: DEFAULT_CONFIG_VALUES.MODELS.FALLBACK
37 | });
38 |
39 | expect(config.storage).toEqual({
40 | type: DEFAULT_CONFIG_VALUES.STORAGE.TYPE,
41 | encoding: DEFAULT_CONFIG_VALUES.STORAGE.ENCODING,
42 | enableBackup: false,
43 | maxBackups: DEFAULT_CONFIG_VALUES.STORAGE.MAX_BACKUPS,
44 | enableCompression: false,
45 | atomicOperations: true
46 | });
47 |
48 | expect(config.version).toBe(DEFAULT_CONFIG_VALUES.VERSION);
49 | });
50 | });
51 |
52 | describe('loadLocalConfig', () => {
53 | it('should load and parse local configuration file', async () => {
54 | const mockConfig = {
55 | models: { main: 'test-model' },
56 | storage: { type: 'api' as const }
57 | };
58 |
59 | vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig));
60 |
61 | const result = await configLoader.loadLocalConfig();
62 |
63 | expect(fs.readFile).toHaveBeenCalledWith(
64 | '/test/project/.taskmaster/config.json',
65 | 'utf-8'
66 | );
67 | expect(result).toEqual(mockConfig);
68 | });
69 |
70 | it('should return null when config file does not exist', async () => {
71 | const error = new Error('File not found') as any;
72 | error.code = 'ENOENT';
73 | vi.mocked(fs.readFile).mockRejectedValue(error);
74 |
75 | const result = await configLoader.loadLocalConfig();
76 |
77 | expect(result).toBeNull();
78 | });
79 |
80 | it('should throw TaskMasterError for other file errors', async () => {
81 | const error = new Error('Permission denied');
82 | vi.mocked(fs.readFile).mockRejectedValue(error);
83 |
84 | await expect(configLoader.loadLocalConfig()).rejects.toThrow(
85 | 'Failed to load local configuration'
86 | );
87 | });
88 |
89 | it('should throw error for invalid JSON', async () => {
90 | vi.mocked(fs.readFile).mockResolvedValue('invalid json');
91 |
92 | await expect(configLoader.loadLocalConfig()).rejects.toThrow();
93 | });
94 | });
95 |
96 | describe('loadGlobalConfig', () => {
97 | it('should return null (not implemented yet)', async () => {
98 | const result = await configLoader.loadGlobalConfig();
99 | expect(result).toBeNull();
100 | });
101 | });
102 |
103 | describe('hasLocalConfig', () => {
104 | it('should return true when local config exists', async () => {
105 | vi.mocked(fs.access).mockResolvedValue(undefined);
106 |
107 | const result = await configLoader.hasLocalConfig();
108 |
109 | expect(fs.access).toHaveBeenCalledWith(
110 | '/test/project/.taskmaster/config.json'
111 | );
112 | expect(result).toBe(true);
113 | });
114 |
115 | it('should return false when local config does not exist', async () => {
116 | vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
117 |
118 | const result = await configLoader.hasLocalConfig();
119 |
120 | expect(result).toBe(false);
121 | });
122 | });
123 |
124 | describe('hasGlobalConfig', () => {
125 | it('should check global config path', async () => {
126 | vi.mocked(fs.access).mockResolvedValue(undefined);
127 |
128 | const result = await configLoader.hasGlobalConfig();
129 |
130 | expect(fs.access).toHaveBeenCalledWith(
131 | expect.stringContaining('.taskmaster/config.json')
132 | );
133 | expect(result).toBe(true);
134 | });
135 |
136 | it('should return false when global config does not exist', async () => {
137 | vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
138 |
139 | const result = await configLoader.hasGlobalConfig();
140 |
141 | expect(result).toBe(false);
142 | });
143 | });
144 | });
145 |
```
--------------------------------------------------------------------------------
/apps/extension/src/components/ui/shadcn-io/kanban/index.tsx:
--------------------------------------------------------------------------------
```typescript
1 | 'use client';
2 |
3 | import { Card } from '@/components/ui/card';
4 | import { cn } from '@/lib/utils';
5 | import {
6 | DndContext,
7 | DragOverlay,
8 | MouseSensor,
9 | TouchSensor,
10 | rectIntersection,
11 | useDraggable,
12 | useDroppable,
13 | useSensor,
14 | useSensors
15 | } from '@dnd-kit/core';
16 | import type { DragEndEvent } from '@dnd-kit/core';
17 | import type React from 'react';
18 | import type { ReactNode } from 'react';
19 |
20 | export type { DragEndEvent } from '@dnd-kit/core';
21 |
22 | export type Status = {
23 | id: string;
24 | name: string;
25 | color: string;
26 | };
27 |
28 | export type Feature = {
29 | id: string;
30 | name: string;
31 | startAt: Date;
32 | endAt: Date;
33 | status: Status;
34 | };
35 |
36 | export type KanbanBoardProps = {
37 | id: Status['id'];
38 | children: ReactNode;
39 | className?: string;
40 | };
41 |
42 | export const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => {
43 | const { isOver, setNodeRef } = useDroppable({ id });
44 |
45 | return (
46 | <div
47 | className={cn(
48 | 'flex h-full min-h-40 flex-col gap-2 rounded-md border bg-secondary p-2 text-xs shadow-sm outline transition-all',
49 | isOver ? 'outline-primary' : 'outline-transparent',
50 | className
51 | )}
52 | ref={setNodeRef}
53 | >
54 | {children}
55 | </div>
56 | );
57 | };
58 |
59 | export type KanbanCardProps = Pick<Feature, 'id' | 'name'> & {
60 | index: number;
61 | parent: string;
62 | children?: ReactNode;
63 | className?: string;
64 | onClick?: (event: React.MouseEvent) => void;
65 | onDoubleClick?: (event: React.MouseEvent) => void;
66 | };
67 |
68 | export const KanbanCard = ({
69 | id,
70 | name,
71 | index,
72 | parent,
73 | children,
74 | className,
75 | onClick,
76 | onDoubleClick
77 | }: KanbanCardProps) => {
78 | const { attributes, listeners, setNodeRef, transform, isDragging } =
79 | useDraggable({
80 | id,
81 | data: { index, parent }
82 | });
83 |
84 | return (
85 | <Card
86 | className={cn(
87 | 'rounded-md p-3 shadow-sm',
88 | isDragging && 'cursor-grabbing opacity-0',
89 | !isDragging && 'cursor-pointer',
90 | className
91 | )}
92 | style={{
93 | transform: transform
94 | ? `translateX(${transform.x}px) translateY(${transform.y}px)`
95 | : 'none'
96 | }}
97 | {...attributes}
98 | {...listeners}
99 | onClick={(e) => !isDragging && onClick?.(e)}
100 | onDoubleClick={onDoubleClick}
101 | ref={setNodeRef}
102 | >
103 | {children ?? <p className="m-0 font-medium text-sm">{name}</p>}
104 | </Card>
105 | );
106 | };
107 |
108 | export type KanbanCardsProps = {
109 | children: ReactNode;
110 | className?: string;
111 | };
112 |
113 | export const KanbanCards = ({ children, className }: KanbanCardsProps) => (
114 | <div className={cn('flex flex-1 flex-col gap-2', className)}>{children}</div>
115 | );
116 |
117 | export type KanbanHeaderProps =
118 | | {
119 | children: ReactNode;
120 | }
121 | | {
122 | name: Status['name'];
123 | color: Status['color'];
124 | className?: string;
125 | };
126 |
127 | export const KanbanHeader = (props: KanbanHeaderProps) =>
128 | 'children' in props ? (
129 | props.children
130 | ) : (
131 | <div className={cn('flex shrink-0 items-center gap-2', props.className)}>
132 | <div
133 | className="h-2 w-2 rounded-full"
134 | style={{ backgroundColor: props.color }}
135 | />
136 | <p className="m-0 font-semibold text-sm">{props.name}</p>
137 | </div>
138 | );
139 |
140 | export type KanbanProviderProps = {
141 | children: ReactNode;
142 | onDragEnd: (event: DragEndEvent) => void;
143 | onDragStart?: (event: DragEndEvent) => void;
144 | onDragCancel?: () => void;
145 | className?: string;
146 | dragOverlay?: ReactNode;
147 | };
148 |
149 | export const KanbanProvider = ({
150 | children,
151 | onDragEnd,
152 | onDragStart,
153 | onDragCancel,
154 | className,
155 | dragOverlay
156 | }: KanbanProviderProps) => {
157 | // Configure sensors with activation constraints to prevent accidental drags
158 | const sensors = useSensors(
159 | // Only start a drag if you've moved more than 8px
160 | useSensor(MouseSensor, {
161 | activationConstraint: { distance: 8 }
162 | }),
163 | // On touch devices, require a short press + small move
164 | useSensor(TouchSensor, {
165 | activationConstraint: { delay: 150, tolerance: 5 }
166 | })
167 | );
168 |
169 | return (
170 | <DndContext
171 | sensors={sensors}
172 | collisionDetection={rectIntersection}
173 | onDragEnd={onDragEnd}
174 | onDragStart={onDragStart}
175 | onDragCancel={onDragCancel}
176 | >
177 | <div
178 | className={cn(
179 | 'grid w-full auto-cols-fr grid-flow-col gap-4',
180 | className
181 | )}
182 | >
183 | {children}
184 | </div>
185 | <DragOverlay>{dragOverlay}</DragOverlay>
186 | </DndContext>
187 | );
188 | };
189 |
```
--------------------------------------------------------------------------------
/.taskmaster/docs/research/2025-06-14_test-save-functionality.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Research Session
3 | query: "test save functionality"
4 | date: 6/14/2025
5 | time: 5:50:23 PM
6 | timestamp: 2025-06-14T21:50:23.797Z
7 | exchanges: 1
8 | ---
9 |
10 | # Research Session
11 |
12 | ## Initial Query
13 |
14 | **Question:** test save functionality
15 |
16 | **Response:**
17 |
18 | ## Testing Save Functionality in a Task Management System
19 |
20 | **Core Steps for Testing Save Functionality**
21 |
22 | - Create or update a task in the system using the CLI or UI.
23 | - Trigger the save operation (e.g., by issuing a command or clicking a save button).
24 | - Verify that the data is correctly written to the persistent storage (e.g., `tasks.json`).
25 | - Reload or re-query the data to ensure the saved changes persist and are accurately reflected.
26 | - Validate that all required fields and relationships (such as dependencies and subtasks) are preserved and correctly structured.
27 |
28 | **Key Test Scenarios**
29 |
30 | - **Basic Save:** Add a new task and confirm it appears in `tasks.json` with all fields populated.
31 | - **Update Save:** Modify an existing task (e.g., change status, description, or priority) and verify the changes are saved.
32 | - **Subtask and Dependency Preservation:** Update a parent task and ensure that completed subtasks and dependency links remain intact, especially after using commands like `updateTask`.
33 | - **Batch Operations:** Perform bulk updates or additions and confirm all affected tasks are saved correctly, with no data loss or corruption.
34 | - **Validation:** Attempt to save invalid data and ensure the system rejects it or provides meaningful error messages, confirming validation logic is enforced.
35 |
36 | **Recommended Practices**
37 |
38 | - Use automated tests to simulate save operations for various scenarios, including edge cases (e.g., circular dependencies, missing required fields).
39 | - After each save, reload the data and compare it to the expected state to ensure integrity.
40 | - Log all save operations and errors for traceability and debugging.
41 | - Regularly back up the `tasks.json` file to prevent data loss during testing and development.
42 |
43 | **Relevant Features from Industry Standards**
44 |
45 | - Task management systems typically provide robust save and update mechanisms, ensuring that all task attributes (including dependencies, subtasks, and comments) are consistently maintained[1][2][5].
46 | - Validation and error handling are critical to prevent corrupt or incomplete saves, especially when handling complex relationships or batch operations[1][5].
47 | - User feedback (such as success messages or error alerts) is important for confirming save actions and guiding users in case of issues[1][2].
48 |
49 | **Example Test Case**
50 |
51 | 1. Create a task with subtasks and dependencies.
52 | 2. Mark some subtasks as complete.
53 | 3. Use the `updateTask` command to modify the parent task.
54 | 4. Save and reload the data.
55 | 5. Assert that:
56 | - The parent task reflects the updates.
57 | - Completed subtasks remain marked as complete.
58 | - Dependencies are unchanged and valid.
59 |
60 | **Summary Table: Save Functionality Test Aspects**
61 |
62 | | Aspect | What to Test | Expected Outcome |
63 | |-----------------------|---------------------------------------------------|-----------------------------------------|
64 | | Basic Save | Add/update task, save, reload | Data persists and matches expectations |
65 | | Subtask Preservation | Update parent, check subtask status | Completed subtasks remain complete |
66 | | Dependency Handling | Save with dependencies, reload | Dependencies are intact and valid |
67 | | Batch Operations | Bulk save/update, reload | All changes persist, no data loss |
68 | | Validation | Save invalid data | Error is raised, data not corrupted |
69 |
70 | These steps and scenarios ensure that the save functionality is robust, reliable, and meets the requirements of a modern task management system[1][2][5].
71 |
72 |
73 | ---
74 |
75 | *Generated by Task Master Research Command*
76 | *Timestamp: 2025-06-14T21:50:23.797Z*
77 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/parse-prd-schema.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { describe, it, expect } from '@jest/globals';
2 | import { prdResponseSchema } from '../../../../../scripts/modules/task-manager/parse-prd/parse-prd-config.js';
3 |
4 | describe('PRD Response Schema', () => {
5 | const validTask = {
6 | id: 1,
7 | title: 'Test Task',
8 | description: 'Test description',
9 | details: 'Test details',
10 | testStrategy: 'Test strategy',
11 | priority: 'high',
12 | dependencies: [],
13 | status: 'pending'
14 | };
15 |
16 | describe('Valid responses', () => {
17 | it('should accept response with tasks and metadata', () => {
18 | const response = {
19 | tasks: [validTask],
20 | metadata: {
21 | projectName: 'Test Project',
22 | totalTasks: 1,
23 | sourceFile: 'test.txt',
24 | generatedAt: '2025-01-01T00:00:00Z'
25 | }
26 | };
27 |
28 | const result = prdResponseSchema.safeParse(response);
29 | expect(result.success).toBe(true);
30 | });
31 |
32 | it('should accept response with tasks and null metadata', () => {
33 | const response = {
34 | tasks: [validTask],
35 | metadata: null
36 | };
37 |
38 | const result = prdResponseSchema.safeParse(response);
39 | expect(result.success).toBe(true);
40 | });
41 |
42 | it('should accept response with only tasks (no metadata field)', () => {
43 | // This is what ZAI returns - just the tasks array without metadata
44 | const response = {
45 | tasks: [validTask]
46 | };
47 |
48 | const result = prdResponseSchema.safeParse(response);
49 | expect(result.success).toBe(true);
50 | if (result.success) {
51 | // With .default(null), omitted metadata becomes null
52 | expect(result.data.metadata).toBeNull();
53 | }
54 | });
55 |
56 | it('should accept response with multiple tasks', () => {
57 | const response = {
58 | tasks: [validTask, { ...validTask, id: 2, title: 'Second Task' }]
59 | };
60 |
61 | const result = prdResponseSchema.safeParse(response);
62 | expect(result.success).toBe(true);
63 | });
64 | });
65 |
66 | describe('Invalid responses', () => {
67 | it('should reject response without tasks field', () => {
68 | const response = {
69 | metadata: null
70 | };
71 |
72 | const result = prdResponseSchema.safeParse(response);
73 | expect(result.success).toBe(false);
74 | });
75 |
76 | it('should reject response with empty tasks array and invalid metadata', () => {
77 | const response = {
78 | tasks: [],
79 | metadata: 'invalid'
80 | };
81 |
82 | const result = prdResponseSchema.safeParse(response);
83 | expect(result.success).toBe(false);
84 | });
85 |
86 | it('should reject task with missing required fields', () => {
87 | const response = {
88 | tasks: [
89 | {
90 | id: 1,
91 | title: 'Test'
92 | // missing other required fields
93 | }
94 | ]
95 | };
96 |
97 | const result = prdResponseSchema.safeParse(response);
98 | expect(result.success).toBe(false);
99 | });
100 |
101 | it('should reject task with invalid priority', () => {
102 | const response = {
103 | tasks: [
104 | {
105 | ...validTask,
106 | priority: 'invalid'
107 | }
108 | ]
109 | };
110 |
111 | const result = prdResponseSchema.safeParse(response);
112 | expect(result.success).toBe(false);
113 | });
114 | });
115 |
116 | describe('ZAI-specific response format', () => {
117 | it('should handle ZAI response format (tasks only, no metadata)', () => {
118 | // This is the actual format ZAI returns
119 | const zaiResponse = {
120 | tasks: [
121 | {
122 | id: 24,
123 | title: 'Core Todo Data Management',
124 | description:
125 | 'Implement the core data structure and CRUD operations',
126 | status: 'pending',
127 | dependencies: [],
128 | priority: 'high',
129 | details: 'Create a Todo data model with properties...',
130 | testStrategy: 'Unit tests for TodoManager class...'
131 | },
132 | {
133 | id: 25,
134 | title: 'Todo UI and User Interactions',
135 | description: 'Create the user interface components',
136 | status: 'pending',
137 | dependencies: [24],
138 | priority: 'high',
139 | details: 'Build a simple HTML/CSS/JS interface...',
140 | testStrategy: 'UI component tests...'
141 | }
142 | ]
143 | };
144 |
145 | const result = prdResponseSchema.safeParse(zaiResponse);
146 | expect(result.success).toBe(true);
147 | if (result.success) {
148 | expect(result.data.tasks).toHaveLength(2);
149 | // With .default(null), omitted metadata becomes null (not undefined)
150 | expect(result.data.metadata).toBeNull();
151 | }
152 | });
153 | });
154 | });
155 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Main entry point for @tm/core
3 | * Provides unified access to all Task Master functionality through TmCore
4 | */
5 |
6 | import type { TasksDomain } from './modules/tasks/tasks-domain.js';
7 |
8 | // ========== Primary API ==========
9 |
10 | /**
11 | * Create a new TmCore instance - The ONLY way to use tm-core
12 | *
13 | * @example
14 | * ```typescript
15 | * import { createTmCore } from '@tm/core';
16 | *
17 | * const tmcore = await createTmCore({
18 | * projectPath: process.cwd()
19 | * });
20 | *
21 | * // Access domains
22 | * await tmcore.auth.login({ ... });
23 | * const tasks = await tmcore.tasks.list();
24 | * await tmcore.workflow.start({ taskId: '1' });
25 | * await tmcore.git.commit('feat: add feature');
26 | * const config = tmcore.config.get('models.main');
27 | * ```
28 | */
29 | export { createTmCore, TmCore, type TmCoreOptions } from './tm-core.js';
30 |
31 | // ========== Type Exports ==========
32 |
33 | // Common types that consumers need
34 | export type * from './common/types/index.js';
35 |
36 | // Common interfaces
37 | export type * from './common/interfaces/index.js';
38 |
39 | // Storage interfaces - TagInfo and TagsWithStatsResult
40 | export type {
41 | TagInfo,
42 | TagsWithStatsResult
43 | } from './common/interfaces/storage.interface.js';
44 |
45 | // Constants
46 | export * from './common/constants/index.js';
47 |
48 | // Errors
49 | export * from './common/errors/index.js';
50 |
51 | // Utils
52 | export * from './common/utils/index.js';
53 | export * from './utils/time.utils.js';
54 |
55 | // ========== Domain-Specific Type Exports ==========
56 |
57 | // Task types
58 | export type {
59 | TaskListResult,
60 | GetTaskListOptions
61 | } from './modules/tasks/services/task-service.js';
62 |
63 | export type {
64 | StartTaskOptions,
65 | StartTaskResult,
66 | ConflictCheckResult
67 | } from './modules/tasks/services/task-execution-service.js';
68 |
69 | export type {
70 | PreflightResult,
71 | CheckResult
72 | } from './modules/tasks/services/preflight-checker.service.js';
73 |
74 | // Task domain result types
75 | export type TaskWithSubtaskResult = Awaited<ReturnType<TasksDomain['get']>>;
76 |
77 | // Auth types
78 | export type {
79 | AuthCredentials,
80 | OAuthFlowOptions,
81 | UserContext
82 | } from './modules/auth/types.js';
83 | export { AuthenticationError } from './modules/auth/types.js';
84 |
85 | // Brief types
86 | export type { Brief } from './modules/briefs/types.js';
87 | export type { TagWithStats } from './modules/briefs/services/brief-service.js';
88 |
89 | // Workflow types
90 | export type {
91 | StartWorkflowOptions,
92 | WorkflowStatus,
93 | NextAction
94 | } from './modules/workflow/services/workflow.service.js';
95 |
96 | export type {
97 | WorkflowPhase,
98 | TDDPhase,
99 | WorkflowContext,
100 | WorkflowState,
101 | TestResult
102 | } from './modules/workflow/types.js';
103 |
104 | // Git types
105 | export type { CommitMessageOptions } from './modules/git/services/commit-message-generator.js';
106 |
107 | // Integration types
108 | export type {
109 | ExportTasksOptions,
110 | ExportResult
111 | } from './modules/integration/services/export.service.js';
112 |
113 | // Reports types
114 | export type {
115 | ComplexityReport,
116 | ComplexityReportMetadata,
117 | ComplexityAnalysis,
118 | TaskComplexityData
119 | } from './modules/reports/types.js';
120 |
121 | // ========== Advanced API (for CLI/Extension/MCP) ==========
122 |
123 | // Auth - Advanced
124 | export { AuthManager } from './modules/auth/managers/auth-manager.js';
125 |
126 | // Briefs - Advanced
127 | export { BriefsDomain } from './modules/briefs/briefs-domain.js';
128 | export { BriefService } from './modules/briefs/services/brief-service.js';
129 |
130 | // Workflow - Advanced
131 | export { WorkflowOrchestrator } from './modules/workflow/orchestrators/workflow-orchestrator.js';
132 | export { WorkflowStateManager } from './modules/workflow/managers/workflow-state-manager.js';
133 | export { WorkflowService } from './modules/workflow/services/workflow.service.js';
134 | export type { SubtaskInfo } from './modules/workflow/types.js';
135 |
136 | // Git - Advanced
137 | export { GitAdapter } from './modules/git/adapters/git-adapter.js';
138 | export { CommitMessageGenerator } from './modules/git/services/commit-message-generator.js';
139 |
140 | // Tasks - Advanced
141 | export { PreflightChecker } from './modules/tasks/services/preflight-checker.service.js';
142 | export { TaskLoaderService } from './modules/tasks/services/task-loader.service.js';
143 |
144 | // Integration - Advanced
145 | export { ExportService } from './modules/integration/services/export.service.js';
146 |
```