This is page 24 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
--------------------------------------------------------------------------------
/scripts/modules/task-manager/parse-prd/parse-prd.js:
--------------------------------------------------------------------------------
```javascript
1 | import chalk from 'chalk';
2 | import {
3 | StreamingError,
4 | STREAMING_ERROR_CODES
5 | } from '../../../../src/utils/stream-parser.js';
6 | import { TimeoutManager } from '../../../../src/utils/timeout-manager.js';
7 | import { getDebugFlag, getDefaultPriority } from '../../config-manager.js';
8 |
9 | // Import configuration classes
10 | import { PrdParseConfig, LoggingConfig } from './parse-prd-config.js';
11 |
12 | // Import helper functions
13 | import {
14 | readPrdContent,
15 | loadExistingTasks,
16 | validateFileOperations,
17 | processTasks,
18 | saveTasksToFile,
19 | buildPrompts,
20 | displayCliSummary,
21 | displayNonStreamingCliOutput
22 | } from './parse-prd-helpers.js';
23 |
24 | // Import handlers
25 | import { handleStreamingService } from './parse-prd-streaming.js';
26 | import { handleNonStreamingService } from './parse-prd-non-streaming.js';
27 |
28 | // ============================================================================
29 | // MAIN PARSING FUNCTIONS (Simplified after refactoring)
30 | // ============================================================================
31 |
32 | /**
33 | * Shared parsing logic for both streaming and non-streaming
34 | * @param {PrdParseConfig} config - Configuration object
35 | * @param {Function} serviceHandler - Handler function for AI service
36 | * @param {boolean} isStreaming - Whether this is streaming mode
37 | * @returns {Promise<Object>} Result object with success status and telemetry
38 | */
39 | async function parsePRDCore(config, serviceHandler, isStreaming) {
40 | const logger = new LoggingConfig(config.mcpLog, config.reportProgress);
41 |
42 | logger.report(
43 | `Parsing PRD file: ${config.prdPath}, Force: ${config.force}, Append: ${config.append}, Research: ${config.research}`,
44 | 'debug'
45 | );
46 |
47 | try {
48 | // Load existing tasks
49 | const { existingTasks, nextId } = loadExistingTasks(
50 | config.tasksPath,
51 | config.targetTag
52 | );
53 |
54 | // Validate operations
55 | validateFileOperations({
56 | existingTasks,
57 | targetTag: config.targetTag,
58 | append: config.append,
59 | force: config.force,
60 | isMCP: config.isMCP,
61 | logger
62 | });
63 |
64 | // Read PRD content and build prompts
65 | const prdContent = readPrdContent(config.prdPath);
66 | const prompts = await buildPrompts(config, prdContent, nextId);
67 |
68 | // Call the appropriate service handler
69 | const serviceResult = await serviceHandler(
70 | config,
71 | prompts,
72 | config.numTasks
73 | );
74 |
75 | // Process tasks
76 | const defaultPriority = getDefaultPriority(config.projectRoot) || 'medium';
77 | const processedNewTasks = processTasks(
78 | serviceResult.parsedTasks,
79 | nextId,
80 | existingTasks,
81 | defaultPriority
82 | );
83 |
84 | // Combine with existing if appending
85 | const finalTasks = config.append
86 | ? [...existingTasks, ...processedNewTasks]
87 | : processedNewTasks;
88 |
89 | // Save to file
90 | saveTasksToFile(config.tasksPath, finalTasks, config.targetTag, logger);
91 |
92 | // Handle completion reporting
93 | await handleCompletionReporting(
94 | config,
95 | serviceResult,
96 | processedNewTasks,
97 | finalTasks,
98 | nextId,
99 | isStreaming
100 | );
101 |
102 | return {
103 | success: true,
104 | tasksPath: config.tasksPath,
105 | telemetryData: serviceResult.aiServiceResponse?.telemetryData,
106 | tagInfo: serviceResult.aiServiceResponse?.tagInfo
107 | };
108 | } catch (error) {
109 | logger.report(`Error parsing PRD: ${error.message}`, 'error');
110 |
111 | if (!config.isMCP) {
112 | console.error(chalk.red(`Error: ${error.message}`));
113 | if (getDebugFlag(config.projectRoot)) {
114 | console.error(error);
115 | }
116 | }
117 | throw error;
118 | }
119 | }
120 |
121 | /**
122 | * Handle completion reporting for both CLI and MCP
123 | * @param {PrdParseConfig} config - Configuration object
124 | * @param {Object} serviceResult - Result from service handler
125 | * @param {Array} processedNewTasks - New tasks that were processed
126 | * @param {Array} finalTasks - All tasks after processing
127 | * @param {number} nextId - Next available task ID
128 | * @param {boolean} isStreaming - Whether this was streaming mode
129 | */
130 | async function handleCompletionReporting(
131 | config,
132 | serviceResult,
133 | processedNewTasks,
134 | finalTasks,
135 | nextId,
136 | isStreaming
137 | ) {
138 | const { aiServiceResponse, estimatedInputTokens, estimatedOutputTokens } =
139 | serviceResult;
140 |
141 | // MCP progress reporting
142 | if (config.reportProgress) {
143 | const hasValidTelemetry =
144 | aiServiceResponse?.telemetryData &&
145 | (aiServiceResponse.telemetryData.inputTokens > 0 ||
146 | aiServiceResponse.telemetryData.outputTokens > 0);
147 |
148 | let completionMessage;
149 | if (hasValidTelemetry) {
150 | const cost = aiServiceResponse.telemetryData.totalCost || 0;
151 | const currency = aiServiceResponse.telemetryData.currency || 'USD';
152 | completionMessage = `✅ Task Generation Completed | Tokens (I/O): ${aiServiceResponse.telemetryData.inputTokens}/${aiServiceResponse.telemetryData.outputTokens} | Cost: ${currency === 'USD' ? '$' : currency}${cost.toFixed(4)}`;
153 | } else {
154 | const outputTokens = isStreaming ? estimatedOutputTokens : 'unknown';
155 | completionMessage = `✅ Task Generation Completed | ~Tokens (I/O): ${estimatedInputTokens}/${outputTokens} | Cost: ~$0.00`;
156 | }
157 |
158 | await config.reportProgress({
159 | progress: config.numTasks,
160 | total: config.numTasks,
161 | message: completionMessage
162 | });
163 | }
164 |
165 | // CLI output
166 | if (config.outputFormat === 'text' && !config.isMCP) {
167 | if (isStreaming && serviceResult.summary) {
168 | await displayCliSummary({
169 | processedTasks: processedNewTasks,
170 | nextId,
171 | summary: serviceResult.summary,
172 | prdPath: config.prdPath,
173 | tasksPath: config.tasksPath,
174 | usedFallback: serviceResult.usedFallback,
175 | aiServiceResponse
176 | });
177 | } else if (!isStreaming) {
178 | displayNonStreamingCliOutput({
179 | processedTasks: processedNewTasks,
180 | research: config.research,
181 | finalTasks,
182 | tasksPath: config.tasksPath,
183 | aiServiceResponse
184 | });
185 | }
186 | }
187 | }
188 |
189 | /**
190 | * Parse PRD with streaming progress reporting
191 | */
192 | async function parsePRDWithStreaming(
193 | prdPath,
194 | tasksPath,
195 | numTasks,
196 | options = {}
197 | ) {
198 | const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options);
199 | return parsePRDCore(config, handleStreamingService, true);
200 | }
201 |
202 | /**
203 | * Parse PRD without streaming (fallback)
204 | */
205 | async function parsePRDWithoutStreaming(
206 | prdPath,
207 | tasksPath,
208 | numTasks,
209 | options = {}
210 | ) {
211 | const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options);
212 | return parsePRDCore(config, handleNonStreamingService, false);
213 | }
214 |
215 | /**
216 | * Main entry point - decides between streaming and non-streaming
217 | */
218 | async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
219 | const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options);
220 |
221 | if (config.useStreaming) {
222 | try {
223 | return await parsePRDWithStreaming(prdPath, tasksPath, numTasks, options);
224 | } catch (streamingError) {
225 | // Check if this is a streaming-specific error (including timeout)
226 | const isStreamingError =
227 | streamingError instanceof StreamingError ||
228 | streamingError.code === STREAMING_ERROR_CODES.NOT_ASYNC_ITERABLE ||
229 | streamingError.code ===
230 | STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED ||
231 | streamingError.code === STREAMING_ERROR_CODES.STREAM_NOT_ITERABLE ||
232 | TimeoutManager.isTimeoutError(streamingError);
233 |
234 | if (isStreamingError) {
235 | const logger = new LoggingConfig(config.mcpLog, config.reportProgress);
236 |
237 | // Show fallback message
238 | if (config.outputFormat === 'text' && !config.isMCP) {
239 | console.log(
240 | chalk.yellow(
241 | `⚠️ Streaming operation ${streamingError.message.includes('timed out') ? 'timed out' : 'failed'}. Falling back to non-streaming mode...`
242 | )
243 | );
244 | } else {
245 | logger.report(
246 | `Streaming failed (${streamingError.message}), falling back to non-streaming mode...`,
247 | 'warn'
248 | );
249 | }
250 |
251 | // Fallback to non-streaming
252 | return await parsePRDWithoutStreaming(
253 | prdPath,
254 | tasksPath,
255 | numTasks,
256 | options
257 | );
258 | } else {
259 | throw streamingError;
260 | }
261 | }
262 | } else {
263 | return await parsePRDWithoutStreaming(
264 | prdPath,
265 | tasksPath,
266 | numTasks,
267 | options
268 | );
269 | }
270 | }
271 |
272 | export default parsePRD;
273 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/clear-subtasks.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Tests for the clear-subtasks.js module
3 | */
4 | import { jest } from '@jest/globals';
5 |
6 | // Mock the dependencies before importing the module under test
7 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
8 | readJSON: jest.fn(),
9 | writeJSON: jest.fn(),
10 | log: jest.fn(),
11 | CONFIG: {
12 | model: 'mock-claude-model',
13 | maxTokens: 4000,
14 | temperature: 0.7,
15 | debug: false
16 | },
17 | findTaskById: jest.fn(),
18 | isSilentMode: jest.fn(() => false),
19 | truncate: jest.fn((text) => text),
20 | ensureTagMetadata: jest.fn()
21 | }));
22 |
23 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
24 | displayBanner: jest.fn()
25 | }));
26 |
27 | jest.unstable_mockModule(
28 | '../../../../../scripts/modules/task-manager/generate-task-files.js',
29 | () => ({
30 | default: jest.fn().mockResolvedValue()
31 | })
32 | );
33 |
34 | // Mock external UI libraries
35 | jest.unstable_mockModule('chalk', () => ({
36 | default: {
37 | white: {
38 | bold: jest.fn((text) => text)
39 | },
40 | cyan: Object.assign(
41 | jest.fn((text) => text),
42 | {
43 | bold: jest.fn((text) => text)
44 | }
45 | ),
46 | green: jest.fn((text) => text),
47 | yellow: jest.fn((text) => text),
48 | bold: jest.fn((text) => text)
49 | }
50 | }));
51 |
52 | jest.unstable_mockModule('boxen', () => ({
53 | default: jest.fn((text) => text)
54 | }));
55 |
56 | jest.unstable_mockModule('cli-table3', () => ({
57 | default: jest.fn().mockImplementation(() => ({
58 | push: jest.fn(),
59 | toString: jest.fn(() => 'mocked table')
60 | }))
61 | }));
62 |
63 | // Mock process.exit to prevent Jest worker crashes
64 | const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => {
65 | throw new Error(`process.exit called with "${code}"`);
66 | });
67 |
68 | // Import the mocked modules
69 | const { readJSON, writeJSON, log, findTaskById, ensureTagMetadata } =
70 | await import('../../../../../scripts/modules/utils.js');
71 | const generateTaskFiles = (
72 | await import(
73 | '../../../../../scripts/modules/task-manager/generate-task-files.js'
74 | )
75 | ).default;
76 |
77 | // Import the module under test
78 | const { default: clearSubtasks } = await import(
79 | '../../../../../scripts/modules/task-manager/clear-subtasks.js'
80 | );
81 |
82 | describe('clearSubtasks', () => {
83 | const sampleTasks = {
84 | master: {
85 | tasks: [
86 | { id: 1, title: 'Task 1', subtasks: [] },
87 | { id: 2, title: 'Task 2', subtasks: [] },
88 | {
89 | id: 3,
90 | title: 'Task 3',
91 | subtasks: [{ id: 1, title: 'Subtask 3.1' }]
92 | },
93 | {
94 | id: 4,
95 | title: 'Task 4',
96 | subtasks: [{ id: 1, title: 'Subtask 4.1' }]
97 | }
98 | ]
99 | }
100 | };
101 |
102 | beforeEach(() => {
103 | jest.clearAllMocks();
104 | mockExit.mockClear();
105 | readJSON.mockImplementation((tasksPath, projectRoot, tag) => {
106 | // Ensure tag contract is honoured
107 | expect(tag).toBeDefined();
108 | expect(tag).toBe('master');
109 | // Create a deep copy to avoid mutation issues between tests
110 | const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
111 | // Return the data for the 'master' tag, which is what the tests use
112 | return {
113 | ...sampleTasksCopy.master,
114 | tag: tag || 'master',
115 | _rawTaggedData: sampleTasksCopy
116 | };
117 | });
118 | writeJSON.mockResolvedValue();
119 | generateTaskFiles.mockResolvedValue();
120 | log.mockImplementation(() => {});
121 | });
122 |
123 | test('should clear subtasks from a specific task', () => {
124 | // Arrange
125 | const taskId = '3';
126 | const tasksPath = 'tasks/tasks.json';
127 | const context = { tag: 'master' };
128 |
129 | // Act
130 | clearSubtasks(tasksPath, taskId, context);
131 |
132 | // Assert
133 | expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master');
134 | expect(writeJSON).toHaveBeenCalledWith(
135 | tasksPath,
136 | expect.objectContaining({
137 | _rawTaggedData: expect.objectContaining({
138 | master: expect.objectContaining({
139 | tasks: expect.arrayContaining([
140 | expect.objectContaining({
141 | id: 3,
142 | subtasks: [] // Should be empty
143 | })
144 | ])
145 | })
146 | })
147 | }),
148 | undefined,
149 | 'master'
150 | );
151 | });
152 |
153 | test('should clear subtasks from multiple tasks when given comma-separated IDs', () => {
154 | // Arrange
155 | const taskIds = '3,4';
156 | const tasksPath = 'tasks/tasks.json';
157 | const context = { tag: 'master' };
158 |
159 | // Act
160 | clearSubtasks(tasksPath, taskIds, context);
161 |
162 | // Assert
163 | expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master');
164 | expect(writeJSON).toHaveBeenCalledWith(
165 | tasksPath,
166 | expect.objectContaining({
167 | _rawTaggedData: expect.objectContaining({
168 | master: expect.objectContaining({
169 | tasks: expect.arrayContaining([
170 | expect.objectContaining({ id: 3, subtasks: [] }),
171 | expect.objectContaining({ id: 4, subtasks: [] })
172 | ])
173 | })
174 | })
175 | }),
176 | undefined,
177 | 'master'
178 | );
179 | });
180 |
181 | test('should handle tasks with no subtasks', () => {
182 | // Arrange
183 | const taskId = '1'; // Task 1 already has no subtasks
184 | const tasksPath = 'tasks/tasks.json';
185 | const context = { tag: 'master' };
186 |
187 | // Act
188 | clearSubtasks(tasksPath, taskId, context);
189 |
190 | // Assert
191 | expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master');
192 | // Should not write the file if no changes were made
193 | expect(writeJSON).not.toHaveBeenCalled();
194 | expect(generateTaskFiles).not.toHaveBeenCalled();
195 | });
196 |
197 | test('should handle non-existent task IDs gracefully', () => {
198 | // Arrange
199 | const taskId = '99'; // Non-existent task
200 | const tasksPath = 'tasks/tasks.json';
201 | const context = { tag: 'master' };
202 |
203 | // Act
204 | clearSubtasks(tasksPath, taskId, context);
205 |
206 | // Assert
207 | expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master');
208 | expect(log).toHaveBeenCalledWith('error', 'Task 99 not found');
209 | // Should not write the file if no changes were made
210 | expect(writeJSON).not.toHaveBeenCalled();
211 | expect(generateTaskFiles).not.toHaveBeenCalled();
212 | });
213 |
214 | test('should handle multiple task IDs including both valid and non-existent IDs', () => {
215 | // Arrange
216 | const taskIds = '3,99'; // Mix of valid and invalid IDs
217 | const tasksPath = 'tasks/tasks.json';
218 | const context = { tag: 'master' };
219 |
220 | // Act
221 | clearSubtasks(tasksPath, taskIds, context);
222 |
223 | // Assert
224 | expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master');
225 | expect(log).toHaveBeenCalledWith('error', 'Task 99 not found');
226 | // Since task 3 has subtasks that should be cleared, writeJSON should be called
227 | expect(writeJSON).toHaveBeenCalledWith(
228 | tasksPath,
229 | expect.objectContaining({
230 | tasks: expect.arrayContaining([
231 | expect.objectContaining({ id: 3, subtasks: [] })
232 | ]),
233 | tag: 'master',
234 | _rawTaggedData: expect.objectContaining({
235 | master: expect.objectContaining({
236 | tasks: expect.arrayContaining([
237 | expect.objectContaining({ id: 3, subtasks: [] })
238 | ])
239 | })
240 | })
241 | }),
242 | undefined,
243 | 'master'
244 | );
245 | });
246 |
247 | test('should handle file read errors', () => {
248 | // Arrange
249 | readJSON.mockImplementation(() => {
250 | throw new Error('File read failed');
251 | });
252 |
253 | // Act & Assert
254 | expect(() => {
255 | clearSubtasks('tasks/tasks.json', '3', { tag: 'master' });
256 | }).toThrow('File read failed');
257 | });
258 |
259 | test('should handle invalid tasks data', () => {
260 | // Arrange
261 | readJSON.mockReturnValue(null);
262 |
263 | // Act & Assert
264 | expect(() => {
265 | clearSubtasks('tasks/tasks.json', '3', { tag: 'master' });
266 | }).toThrow('process.exit called');
267 |
268 | expect(log).toHaveBeenCalledWith('error', 'No valid tasks found.');
269 | });
270 |
271 | test('should handle file write errors', () => {
272 | // Arrange
273 | // Ensure task 3 has subtasks to clear so writeJSON gets called
274 | readJSON.mockReturnValue({
275 | ...sampleTasks.master,
276 | tag: 'master',
277 | _rawTaggedData: sampleTasks,
278 | tasks: [
279 | ...sampleTasks.master.tasks.slice(0, 2),
280 | {
281 | ...sampleTasks.master.tasks[2],
282 | subtasks: [{ id: 1, title: 'Subtask to clear' }]
283 | },
284 | ...sampleTasks.master.tasks.slice(3)
285 | ]
286 | });
287 |
288 | writeJSON.mockImplementation(() => {
289 | throw new Error('File write failed');
290 | });
291 |
292 | // Act & Assert
293 | expect(() => {
294 | clearSubtasks('tasks/tasks.json', '3', { tag: 'master' });
295 | }).toThrow('File write failed');
296 | });
297 | });
298 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/utils/run-id-generator.spec.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { describe, expect, it } from 'vitest';
2 | import {
3 | compareRunIds,
4 | generateRunId,
5 | isValidRunId,
6 | parseRunId
7 | } from './run-id-generator.js';
8 |
9 | describe('Run ID Generator', () => {
10 | describe('generateRunId', () => {
11 | it('should generate a valid ISO 8601 timestamp-based ID', () => {
12 | const runId = generateRunId();
13 |
14 | // Should be in ISO 8601 format with milliseconds
15 | expect(runId).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
16 | });
17 |
18 | it('should generate unique IDs when called multiple times', () => {
19 | const id1 = generateRunId();
20 | const id2 = generateRunId();
21 | const id3 = generateRunId();
22 |
23 | expect(id1).not.toBe(id2);
24 | expect(id2).not.toBe(id3);
25 | expect(id1).not.toBe(id3);
26 | });
27 |
28 | it('should generate chronologically ordered IDs', () => {
29 | const id1 = generateRunId();
30 | // Small delay to ensure different timestamp
31 | const id2 = generateRunId();
32 |
33 | expect(id2 > id1).toBe(true);
34 | });
35 |
36 | it('should use current time by default', () => {
37 | const before = new Date().toISOString();
38 | const runId = generateRunId();
39 | const after = new Date().toISOString();
40 |
41 | expect(runId >= before).toBe(true);
42 | expect(runId <= after).toBe(true);
43 | });
44 |
45 | it('should accept custom Date object', () => {
46 | const customDate = new Date('2024-01-15T10:30:45.123Z');
47 | const runId = generateRunId(customDate);
48 |
49 | expect(runId).toBe('2024-01-15T10:30:45.123Z');
50 | });
51 |
52 | it('should handle date at year boundary', () => {
53 | const newYear = new Date('2025-01-01T00:00:00.000Z');
54 | const runId = generateRunId(newYear);
55 |
56 | expect(runId).toBe('2025-01-01T00:00:00.000Z');
57 | });
58 |
59 | it('should handle millisecond precision correctly', () => {
60 | const dateWithMs = new Date('2024-03-15T14:22:33.999Z');
61 | const runId = generateRunId(dateWithMs);
62 |
63 | expect(runId).toBe('2024-03-15T14:22:33.999Z');
64 | });
65 | });
66 |
67 | describe('isValidRunId', () => {
68 | it('should return true for valid ISO 8601 timestamp', () => {
69 | expect(isValidRunId('2024-01-15T10:30:45.123Z')).toBe(true);
70 | });
71 |
72 | it('should return true for generated run IDs', () => {
73 | const runId = generateRunId();
74 | expect(isValidRunId(runId)).toBe(true);
75 | });
76 |
77 | it('should return false for invalid format', () => {
78 | expect(isValidRunId('not-a-timestamp')).toBe(false);
79 | expect(isValidRunId('2024-01-15')).toBe(false);
80 | expect(isValidRunId('2024-01-15T10:30:45')).toBe(false); // missing Z
81 | expect(isValidRunId('2024-01-15 10:30:45.123Z')).toBe(false); // space instead of T
82 | });
83 |
84 | it('should return false for empty string', () => {
85 | expect(isValidRunId('')).toBe(false);
86 | });
87 |
88 | it('should return false for null or undefined', () => {
89 | expect(isValidRunId(null)).toBe(false);
90 | expect(isValidRunId(undefined)).toBe(false);
91 | });
92 |
93 | it('should return false for invalid dates', () => {
94 | expect(isValidRunId('2024-13-01T10:30:45.123Z')).toBe(false); // invalid month
95 | expect(isValidRunId('2024-01-32T10:30:45.123Z')).toBe(false); // invalid day
96 | expect(isValidRunId('2024-01-15T25:30:45.123Z')).toBe(false); // invalid hour
97 | });
98 |
99 | it('should return true for edge case valid dates', () => {
100 | expect(isValidRunId('2024-02-29T23:59:59.999Z')).toBe(true); // leap year
101 | expect(isValidRunId('2025-01-01T00:00:00.000Z')).toBe(true); // year boundary
102 | });
103 |
104 | it('should return false for missing milliseconds', () => {
105 | expect(isValidRunId('2024-01-15T10:30:45Z')).toBe(false);
106 | });
107 |
108 | it('should return false for non-UTC timezone', () => {
109 | expect(isValidRunId('2024-01-15T10:30:45.123+01:00')).toBe(false);
110 | });
111 | });
112 |
113 | describe('parseRunId', () => {
114 | it('should parse valid run ID to Date object', () => {
115 | const runId = '2024-01-15T10:30:45.123Z';
116 | const date = parseRunId(runId);
117 |
118 | expect(date).toBeInstanceOf(Date);
119 | expect(date?.toISOString()).toBe(runId);
120 | });
121 |
122 | it('should parse generated run ID', () => {
123 | const originalDate = new Date('2024-03-20T15:45:30.500Z');
124 | const runId = generateRunId(originalDate);
125 | const parsedDate = parseRunId(runId);
126 |
127 | expect(parsedDate?.getTime()).toBe(originalDate.getTime());
128 | });
129 |
130 | it('should return null for invalid run ID', () => {
131 | expect(parseRunId('invalid')).toBe(null);
132 | expect(parseRunId('')).toBe(null);
133 | expect(parseRunId(null)).toBe(null);
134 | expect(parseRunId(undefined)).toBe(null);
135 | });
136 |
137 | it('should handle edge case dates correctly', () => {
138 | const leapYear = '2024-02-29T12:00:00.000Z';
139 | const parsed = parseRunId(leapYear);
140 |
141 | expect(parsed?.toISOString()).toBe(leapYear);
142 | });
143 | });
144 |
145 | describe('compareRunIds', () => {
146 | it('should return negative when first ID is earlier', () => {
147 | const earlier = '2024-01-15T10:00:00.000Z';
148 | const later = '2024-01-15T11:00:00.000Z';
149 |
150 | expect(compareRunIds(earlier, later)).toBeLessThan(0);
151 | });
152 |
153 | it('should return positive when first ID is later', () => {
154 | const earlier = '2024-01-15T10:00:00.000Z';
155 | const later = '2024-01-15T11:00:00.000Z';
156 |
157 | expect(compareRunIds(later, earlier)).toBeGreaterThan(0);
158 | });
159 |
160 | it('should return zero when IDs are equal', () => {
161 | const runId = '2024-01-15T10:00:00.000Z';
162 |
163 | expect(compareRunIds(runId, runId)).toBe(0);
164 | });
165 |
166 | it('should handle millisecond differences', () => {
167 | const id1 = '2024-01-15T10:00:00.100Z';
168 | const id2 = '2024-01-15T10:00:00.200Z';
169 |
170 | expect(compareRunIds(id1, id2)).toBeLessThan(0);
171 | expect(compareRunIds(id2, id1)).toBeGreaterThan(0);
172 | });
173 |
174 | it('should handle cross-day comparisons', () => {
175 | const yesterday = '2024-01-14T23:59:59.999Z';
176 | const today = '2024-01-15T00:00:00.000Z';
177 |
178 | expect(compareRunIds(yesterday, today)).toBeLessThan(0);
179 | });
180 |
181 | it('should handle cross-year comparisons', () => {
182 | const lastYear = '2023-12-31T23:59:59.999Z';
183 | const thisYear = '2024-01-01T00:00:00.000Z';
184 |
185 | expect(compareRunIds(lastYear, thisYear)).toBeLessThan(0);
186 | });
187 |
188 | it('should throw error for invalid run IDs', () => {
189 | const valid = '2024-01-15T10:00:00.000Z';
190 |
191 | expect(() => compareRunIds('invalid', valid)).toThrow();
192 | expect(() => compareRunIds(valid, 'invalid')).toThrow();
193 | expect(() => compareRunIds('invalid', 'invalid')).toThrow();
194 | });
195 | });
196 |
197 | describe('Collision detection', () => {
198 | it('should generate different IDs in rapid succession', () => {
199 | const ids = new Set();
200 | const count = 100;
201 |
202 | for (let i = 0; i < count; i++) {
203 | ids.add(generateRunId());
204 | }
205 |
206 | // All IDs should be unique
207 | expect(ids.size).toBe(count);
208 | });
209 |
210 | it('should handle high-frequency generation', () => {
211 | const ids = [];
212 | const iterations = 1000;
213 |
214 | for (let i = 0; i < iterations; i++) {
215 | ids.push(generateRunId());
216 | }
217 |
218 | // Check uniqueness
219 | const uniqueIds = new Set(ids);
220 | expect(uniqueIds.size).toBe(iterations);
221 |
222 | // Check chronological order
223 | for (let i = 1; i < ids.length; i++) {
224 | expect(compareRunIds(ids[i - 1], ids[i])).toBeLessThanOrEqual(0);
225 | }
226 | });
227 | });
228 |
229 | describe('Chronological ordering', () => {
230 | it('should allow sorting run IDs chronologically', () => {
231 | const ids = [
232 | '2024-01-15T14:00:00.000Z',
233 | '2024-01-15T10:00:00.000Z',
234 | '2024-01-15T12:00:00.000Z',
235 | '2024-01-14T23:00:00.000Z',
236 | '2024-01-16T08:00:00.000Z'
237 | ];
238 |
239 | const sorted = [...ids].sort(compareRunIds);
240 |
241 | expect(sorted).toEqual([
242 | '2024-01-14T23:00:00.000Z',
243 | '2024-01-15T10:00:00.000Z',
244 | '2024-01-15T12:00:00.000Z',
245 | '2024-01-15T14:00:00.000Z',
246 | '2024-01-16T08:00:00.000Z'
247 | ]);
248 | });
249 |
250 | it('should handle reverse chronological sorting', () => {
251 | const ids = [
252 | '2024-01-15T10:00:00.000Z',
253 | '2024-01-15T14:00:00.000Z',
254 | '2024-01-15T12:00:00.000Z'
255 | ];
256 |
257 | const sorted = [...ids].sort((a, b) => compareRunIds(b, a));
258 |
259 | expect(sorted).toEqual([
260 | '2024-01-15T14:00:00.000Z',
261 | '2024-01-15T12:00:00.000Z',
262 | '2024-01-15T10:00:00.000Z'
263 | ]);
264 | });
265 | });
266 | });
267 |
```
--------------------------------------------------------------------------------
/.taskmaster/docs/research/2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Research Session
3 | query: "how can i improve the scope-up and scope-down command"
4 | date: 6/14/2025
5 | time: 4:35:03 PM
6 | timestamp: 2025-06-14T20:35:03.231Z
7 | exchanges: 2
8 | ---
9 |
10 | # Research Session
11 |
12 | ## Initial Query
13 |
14 | **Question:** how can i improve the scope-up and scope-down command
15 |
16 | **Response:**
17 |
18 | ## Improving the `scope-up` and `scope-down` Commands
19 |
20 | **Clarify Command Purpose and User Experience**
21 |
22 | - Ensure the commands have clear, intuitive behavior. For example, `scope-up` should consistently broaden the context or task view, while `scope-down` should reliably narrow it.
23 | - Provide immediate, user-friendly feedback after each command, indicating what changed (e.g., "Now viewing parent task and all subtasks" or "Focused on subtask: X").
24 | - Document the commands with examples in your `docs/command-reference.md` and `docs/examples.md` to help users understand their effects and use cases.
25 |
26 | **Enhance Context Awareness**
27 |
28 | - Make the commands context-sensitive: if the user is already at the top or bottom of the scope hierarchy, return a helpful message instead of failing silently.
29 | - Allow optional arguments to specify the exact level or target (e.g., `scope-up 2` to move up two levels, or `scope-down <subtask-id>` to jump directly to a subtask).
30 |
31 | **Robust Error Handling and Validation**
32 |
33 | - Implement checks to prevent invalid operations, such as attempting to scope up from the root or scope down when no subtasks exist.
34 | - Provide clear error messages and suggestions for next steps, improving user trust and reducing confusion[1][5].
35 |
36 | **Integrate with Task and Tag Systems**
37 |
38 | - Ensure `scope-up` and `scope-down` interact smoothly with your tagging and task hierarchy features. For example, scoping up from a subtask should show the parent task and its context, while scoping down should allow selection from available subtasks or related tags.
39 | - Consider supporting batch operations, such as scoping down into all subtasks and displaying them in a summarized view.
40 |
41 | **Improve Performance and Synchronization**
42 |
43 | - Optimize the underlying logic to minimize delays, especially in large task trees. Use asynchronous operations where possible to keep the UI responsive.
44 | - If the commands trigger data loading or context switching, ensure synchronization so that subsequent commands operate on the correct, updated context[2][4].
45 |
46 | **Testing and Stakeholder Feedback**
47 |
48 | - Add or expand unit and integration tests for these commands (see `tests/unit/commands.test.js` and `integration/cli/commands.test.js`) to cover edge cases and ensure reliability.
49 | - Gather feedback from users and stakeholders to identify pain points and desired enhancements, then iterate on the command design and implementation[1][5].
50 |
51 | **Example Improvements Table**
52 |
53 | | Area | Current Issue | Suggested Improvement |
54 | |---------------------|-----------------------------|----------------------------------------------|
55 | | User Feedback | Minimal or unclear output | Add explicit, contextual messages |
56 | | Error Handling | Silent failures | Informative errors and recovery suggestions |
57 | | Flexibility | Only single-level movement | Support multi-level and targeted navigation |
58 | | Integration | Limited context awareness | Sync with tags, parent/child relationships |
59 | | Performance | Slow on large hierarchies | Optimize data access and UI updates |
60 |
61 | By focusing on these areas, you can make `scope-up` and `scope-down` more robust, user-friendly, and aligned with best practices in scope management and command design[1][5].
62 |
63 | ---
64 |
65 | ## Follow-up 1
66 |
67 | **Question:** how might i do this relative to the tag system?
68 |
69 | **Response:**
70 |
71 | ## Integrating Scope-Up and Scope-Down with the Tag System
72 |
73 | **Conceptual Mapping**
74 |
75 | - In your project, *scope-up* and *scope-down* commands currently operate on the task hierarchy, broadening or narrowing the context by moving between parent and child tasks.
76 | - The tag system provides a parallel, orthogonal way to group and filter tasks—tags can represent categories, priorities, or any custom grouping, independent of strict parent/child relationships.
77 |
78 | **How to Make Scope Commands Tag-Aware**
79 |
80 | - **Scoping by Tag**: Allow users to "scope-down" into a tag, viewing only tasks associated with that tag. Conversely, "scope-up" could move from a specific tag context back to the broader task list or to a parent tag grouping if you support tag hierarchies.
81 | - **Tag-Scoped Views**: When a tag is active as the current scope, all navigation and operations (listing, editing, etc.) are limited to tasks with that tag. This is similar to how some systems let you set the "Tag Scope" to filter navigation and search to only tagged items[1][5].
82 | - **Combining Hierarchy and Tags**: Enable compound scoping, such as "all subtasks of Task X that also have Tag Y." This can be implemented by intersecting the results of hierarchical and tag-based queries.
83 |
84 | **Implementation Strategies**
85 |
86 | - **Scope Stack or Context Object**: Maintain a stack or context object that tracks both the current task scope and any active tag filters. Each scope change (up/down or tag in/out) updates this context.
87 | - **Commands**:
88 | - `scope-down --tag <tag>`: Narrows the view to tasks with the specified tag.
89 | - `scope-up --tag`: Removes the tag filter, broadening the view.
90 | - `scope-down <subtask-id> --tag <tag>`: Narrows to a specific subtask *and* applies a tag filter.
91 | - **Tag Hierarchies (Optional)**: If your tag system supports parent/child tags or tag groups, allow "scope-up" to move from a specific tag to its parent tag or group, and "scope-down" to move into a child tag[4][5].
92 |
93 | **Example User Flows**
94 |
95 | - *Scoping Down by Tag*:
96 | User runs `scope-down --tag urgent` → Only tasks tagged "urgent" are shown.
97 | - *Combining Task and Tag*:
98 | User runs `scope-down <task-id> --tag frontend` → Only subtasks of `<task-id>` that are tagged "frontend" are shown.
99 | - *Scoping Up from Tag*:
100 | User runs `scope-up --tag` → Removes the tag filter, returning to the previous broader context.
101 |
102 | **Technical Considerations**
103 |
104 | - **Efficient Filtering**: Use indexed queries or in-memory filtering to efficiently combine tag and hierarchy filters, especially for large task sets.
105 | - **Feedback and UI**: Always display the current scope context (e.g., "Viewing tasks tagged 'urgent' under 'Feature X'") so users understand what they're seeing.
106 | - **Extensibility**: Design the scope system so new types of scopes (e.g., by status, assignee) can be added in the future, not just tags and hierarchy.
107 |
108 | **Relevant Precedents**
109 |
110 | - Tag-based file systems and management tools often implement "scopes" as dynamic groups defined by tag criteria, allowing users to build up complex views by combining tags and other filters[5].
111 | - Some systems allow scoping into tag management itself, where you can create, edit, or view tags and their associated objects, which could be mirrored in your CLI or UI[4].
112 |
113 | **Summary Table**
114 |
115 | | Command Example | Resulting Scope |
116 | |----------------------------------|--------------------------------------------------|
117 | | `scope-down --tag bug` | All tasks tagged "bug" |
118 | | `scope-down <task-id> --tag api` | Subtasks of `<task-id>` tagged "api" |
119 | | `scope-up --tag` | Removes tag filter, returns to previous scope |
120 | | `scope-down --tag frontend` | All tasks tagged "frontend" |
121 |
122 | By integrating tag-based scoping, you make your scope commands more flexible and powerful, supporting both hierarchical and ad-hoc organizational strategies[1][5].
123 |
124 |
125 | ---
126 |
127 | *Generated by Task Master Research Command*
128 | *Timestamp: 2025-06-14T20:35:03.231Z*
129 |
```
--------------------------------------------------------------------------------
/apps/extension/docs/extension-development-guide.md:
--------------------------------------------------------------------------------
```markdown
1 | # VS Code Extension Development Guide
2 |
3 | ## 📁 File Structure Overview
4 |
5 | This VS Code extension uses a **3-file packaging system** to avoid dependency conflicts during publishing:
6 |
7 | ```
8 | apps/extension/
9 | ├── package.json # Development configuration
10 | ├── package.publish.json # Clean publishing configuration
11 | ├── package.mjs # Build script for packaging
12 | ├── .vscodeignore # Files to exclude from extension package
13 | └── vsix-build/ # Generated clean package directory
14 | ```
15 |
16 | ## 📋 File Purposes
17 |
18 | ### `package.json` (Development)
19 | - **Purpose**: Development environment with all build tools
20 | - **Contains**:
21 | - All `devDependencies` needed for building
22 | - Development scripts (`build`, `watch`, `lint`, etc.)
23 | - Development package name: `"taskr"`
24 | - **Used for**: Local development, building, testing
25 |
26 | ### `package.publish.json` (Publishing)
27 | - **Purpose**: Clean distribution version for VS Code Marketplace
28 | - **Contains**:
29 | - **No devDependencies** (avoids dependency conflicts)
30 | - Publishing metadata (`keywords`, `repository`, `categories`)
31 | - Marketplace package name: `"taskr-kanban"`
32 | - VS Code extension configuration
33 | - **Used for**: Final extension packaging
34 |
35 | ### `package.mjs` (Build Script)
36 | - **Purpose**: Creates clean package for distribution
37 | - **Process**:
38 | 1. Builds the extension (`build:js` + `build:css`)
39 | 2. Creates clean `vsix-build/` directory
40 | 3. Copies only essential files (no source code)
41 | 4. Renames `package.publish.json` → `package.json`
42 | 5. Ready for `vsce package`
43 |
44 | ## 🚀 Development Workflow
45 |
46 | ### Local Development
47 | ```bash
48 | # Install dependencies
49 | npm install
50 |
51 | # Start development with hot reload
52 | npm run watch
53 |
54 | # Run just JavaScript build
55 | npm run build:js
56 |
57 | # Run just CSS build
58 | npm run build:css
59 |
60 | # Full production build
61 | npm run build
62 |
63 | # Type checking
64 | npm run typecheck
65 |
66 | # Linting
67 | npm run lint
68 | ```
69 |
70 | ### Testing in VS Code
71 | 1. Press `F5` in VS Code to launch Extension Development Host
72 | 2. Test your extension functionality in the new window
73 | 3. Use `Developer: Reload Window` to reload after changes
74 |
75 | ## 📦 Production Packaging
76 |
77 | ### Step 1: Build Clean Package
78 | ```bash
79 | npm run package
80 | ```
81 | This creates `vsix-build/` with clean distribution files.
82 |
83 | ### Step 2: Create VSIX
84 | ```bash
85 | cd vsix-build
86 | npx vsce package --no-dependencies
87 | ```
88 | Creates: `taskr-kanban-1.0.1.vsix`
89 |
90 | ### Alternative: One Command
91 | ```bash
92 | npm run package && cd vsix-build && npx vsce package --no-dependencies
93 | ```
94 |
95 | ## 🔄 Keeping Files in Sync
96 |
97 | ### Critical Fields to Sync Between Files
98 |
99 | When updating extension metadata, ensure these fields match between `package.json` and `package.publish.json`:
100 |
101 | #### Version & Identity
102 | ```json
103 | {
104 | "version": "1.0.1", // ⚠️ MUST MATCH
105 | "publisher": "Hamster", // ⚠️ MUST MATCH
106 | "displayName": "taskr: Task Master Kanban", // ⚠️ MUST MATCH
107 | "description": "A visual Kanban board...", // ⚠️ MUST MATCH
108 | }
109 | ```
110 |
111 | #### VS Code Configuration
112 | ```json
113 | {
114 | "engines": { "vscode": "^1.101.0" }, // ⚠️ MUST MATCH
115 | "categories": [...], // ⚠️ MUST MATCH
116 | "activationEvents": [...], // ⚠️ MUST MATCH
117 | "main": "./dist/extension.js", // ⚠️ MUST MATCH
118 | "contributes": { ... } // ⚠️ MUST MATCH EXACTLY
119 | }
120 | ```
121 |
122 | ### Key Differences (Should NOT Match)
123 | ```json
124 | // package.json (dev)
125 | {
126 | "name": "taskr", // ✅ Short dev name
127 | "devDependencies": { ... }, // ✅ Only in dev file
128 | "scripts": { ... } // ✅ Build scripts
129 | }
130 |
131 | // package.publish.json (publishing)
132 | {
133 | "name": "taskr-kanban", // ✅ Marketplace name
134 | "keywords": [...], // ✅ Only in publish file
135 | "repository": "https://github.com/...", // ✅ Only in publish file
136 | // NO devDependencies // ✅ Clean for publishing
137 | // NO build scripts // ✅ Not needed in package
138 | }
139 | ```
140 |
141 | ## 🤖 Automated Release Process
142 |
143 | ### Changesets Workflow
144 | This extension uses [Changesets](https://github.com/changesets/changesets) for automated version management and publishing.
145 |
146 | #### Adding Changes
147 | When making changes to the extension:
148 |
149 | 1. **Make your code changes**
150 | 2. **Create a changeset**:
151 | ```bash
152 | # From project root
153 | npx changeset add
154 | ```
155 | 3. **Select the extension package**: Choose `taskr-kanban` when prompted
156 | 4. **Select version bump type**:
157 | - `patch`: Bug fixes, minor updates
158 | - `minor`: New features, backwards compatible
159 | - `major`: Breaking changes
160 | 5. **Write a summary**: Describe what changed for users
161 |
162 | #### Automated Publishing
163 | The automation workflow runs on pushes to `main`:
164 |
165 | 1. **Version Workflow** (`.github/workflows/version.yml`):
166 | - Detects when changesets exist
167 | - Creates a "Version Packages" PR with updated versions and CHANGELOG
168 | - When the PR is merged, automatically publishes the extension
169 |
170 | 2. **Release Process** (`scripts/release.sh`):
171 | - Builds the extension using the 3-file packaging system
172 | - Creates VSIX package
173 | - Publishes to VS Code Marketplace (if `VSCE_PAT` is set)
174 | - Publishes to Open VSX Registry (if `OVSX_PAT` is set)
175 | - Creates git tags for the extension version
176 |
177 | #### Required Secrets
178 | For automated publishing, these secrets must be set in the repository:
179 |
180 | - `VSCE_PAT`: Personal Access Token for VS Code Marketplace
181 | - `OVSX_PAT`: Personal Access Token for Open VSX Registry
182 | - `GITHUB_TOKEN`: Automatically provided by GitHub Actions
183 |
184 | #### Manual Release
185 | If needed, you can manually trigger a release:
186 |
187 | ```bash
188 | # From project root
189 | ./scripts/release.sh
190 | ```
191 |
192 | ### Extension Tagging
193 | The extension uses a separate tagging strategy from the main package:
194 |
195 | - **Extension tags**: `[email protected]`
196 | - **Main package tags**: `[email protected]`
197 |
198 | This allows independent versioning and prevents conflicts in the monorepo.
199 |
200 | ## 🔍 Troubleshooting
201 |
202 | ### Dependency Conflicts
203 | **Problem**: `vsce package` fails with missing dependencies
204 | **Solution**: Use the 3-file system - never run `vsce package` from root
205 |
206 | ### Build Failures
207 | **Problem**: Extension not working after build
208 | **Check**:
209 | 1. All files copied to `vsix-build/dist/`
210 | 2. `package.publish.json` has correct `main` field
211 | 3. VS Code engine version compatibility
212 |
213 | ### Sync Issues
214 | **Problem**: Extension works locally but fails when packaged
215 | **Check**: Ensure critical fields are synced between package files
216 |
217 | ### Changeset Issues
218 | **Problem**: Version workflow not triggering
219 | **Check**:
220 | 1. Changeset files exist in `.changeset/`
221 | 2. Package name in changeset matches `package.publish.json`
222 | 3. Changes are pushed to `main` branch
223 |
224 | **Problem**: Publishing fails
225 | **Check**:
226 | 1. Required secrets are set in repository settings
227 | 2. `package.publish.json` has correct repository URL
228 | 3. Build process completes successfully
229 |
230 | ## 📝 Version Release Checklist
231 |
232 | ### Manual Releases
233 | 1. **Create changeset**: `npx changeset add`
234 | 2. **Update critical fields** in both `package.json` and `package.publish.json`
235 | 3. **Test locally** with `F5` in VS Code
236 | 4. **Commit and push** to trigger automated workflow
237 |
238 | ### Automated Releases (Recommended)
239 | 1. **Create changeset**: `npx changeset add`
240 | 2. **Push to feature branch** and create PR
241 | 3. **Merge PR** - this triggers version PR creation
242 | 4. **Review and merge version PR** - this triggers automated publishing
243 |
244 | ## 🎯 Why This System?
245 |
246 | - **Avoids dependency conflicts**: VS Code doesn't see dev dependencies
247 | - **Clean distribution**: Only essential files in final package
248 | - **Faster packaging**: No dependency resolution during `vsce package`
249 | - **Maintainable**: Clear separation of dev vs. production configs
250 | - **Reliable**: Consistent, conflict-free packaging process
251 | - **Automated**: Changesets handle versioning and publishing automatically
252 | - **Traceable**: Clear changelog and git tags for every release
253 |
254 | ---
255 |
256 | **Remember**: Always use `npx changeset add` for changes, then push to trigger automated releases! 🚀
257 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/common/logger/logger.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Core logger implementation
3 | */
4 |
5 | import chalk from 'chalk';
6 |
7 | export enum LogLevel {
8 | SILENT = 0,
9 | ERROR = 1,
10 | WARN = 2,
11 | INFO = 3,
12 | DEBUG = 4
13 | }
14 |
15 | /**
16 | * Log object interface (e.g., MCP context.log)
17 | */
18 | export interface LogObject {
19 | info: (message: string) => void;
20 | warn: (message: string) => void;
21 | error: (message: string) => void;
22 | debug: (message: string) => void;
23 | }
24 |
25 | /**
26 | * Log callback can be either a function or a log object
27 | */
28 | export type LogCallback =
29 | | ((level: string, message: string) => void)
30 | | LogObject;
31 |
32 | export interface LoggerConfig {
33 | level?: LogLevel;
34 | silent?: boolean;
35 | prefix?: string;
36 | timestamp?: boolean;
37 | colors?: boolean;
38 | // MCP mode silences all output (unless logCallback is provided)
39 | mcpMode?: boolean;
40 | // Callback function or object for logging (useful for MCP integration)
41 | logCallback?: LogCallback;
42 | }
43 |
44 | export class Logger {
45 | private config: LoggerConfig & {
46 | level: LogLevel;
47 | silent: boolean;
48 | prefix: string;
49 | timestamp: boolean;
50 | colors: boolean;
51 | mcpMode: boolean;
52 | };
53 | private static readonly DEFAULT_CONFIG = {
54 | level: LogLevel.SILENT,
55 | silent: false,
56 | prefix: '',
57 | timestamp: false,
58 | colors: true,
59 | mcpMode: false,
60 | logCallback: undefined as LogCallback | undefined
61 | };
62 |
63 | constructor(config: LoggerConfig = {}) {
64 | // Check environment variables
65 | const envConfig: LoggerConfig = {};
66 |
67 | // Check for MCP mode
68 | if (
69 | process.env.MCP_MODE === 'true' ||
70 | process.env.TASK_MASTER_MCP === 'true'
71 | ) {
72 | envConfig.mcpMode = true;
73 | }
74 |
75 | // Check for silent mode
76 | if (
77 | process.env.TASK_MASTER_SILENT === 'true' ||
78 | process.env.TM_SILENT === 'true'
79 | ) {
80 | envConfig.silent = true;
81 | }
82 |
83 | // Check for log level
84 | if (process.env.TASK_MASTER_LOG_LEVEL || process.env.TM_LOG_LEVEL) {
85 | const levelStr = (
86 | process.env.TASK_MASTER_LOG_LEVEL ||
87 | process.env.TM_LOG_LEVEL ||
88 | ''
89 | ).toUpperCase();
90 | if (levelStr in LogLevel) {
91 | envConfig.level = LogLevel[levelStr as keyof typeof LogLevel];
92 | }
93 | }
94 |
95 | // Check for no colors
96 | if (
97 | process.env.NO_COLOR === 'true' ||
98 | process.env.TASK_MASTER_NO_COLOR === 'true'
99 | ) {
100 | envConfig.colors = false;
101 | }
102 |
103 | // Merge configs: defaults < constructor < environment
104 | this.config = {
105 | ...Logger.DEFAULT_CONFIG,
106 | ...config,
107 | ...envConfig
108 | };
109 |
110 | // MCP mode overrides to silent ONLY if no callback is provided
111 | if (this.config.mcpMode && !this.config.logCallback) {
112 | this.config.silent = true;
113 | }
114 | }
115 |
116 | /**
117 | * Check if logging is enabled for a given level
118 | */
119 | private shouldLog(level: LogLevel): boolean {
120 | // If a callback is provided, route logs through it while still respecting the configured level
121 | if (this.config.logCallback) {
122 | return level <= this.config.level;
123 | }
124 |
125 | // Otherwise, respect silent/mcpMode flags
126 | if (this.config.silent || this.config.mcpMode) {
127 | return false;
128 | }
129 | return level <= this.config.level;
130 | }
131 |
132 | /**
133 | * Format a log message
134 | */
135 | private formatMessage(
136 | level: LogLevel,
137 | message: string,
138 | ...args: any[]
139 | ): string {
140 | let formatted = '';
141 |
142 | // Add timestamp if enabled
143 | if (this.config.timestamp) {
144 | const timestamp = new Date().toISOString();
145 | formatted += this.config.colors
146 | ? chalk.gray(`[${timestamp}] `)
147 | : `[${timestamp}] `;
148 | }
149 |
150 | // Add prefix if configured
151 | if (this.config.prefix) {
152 | formatted += this.config.colors
153 | ? chalk.cyan(`[${this.config.prefix}] `)
154 | : `[${this.config.prefix}] `;
155 | }
156 |
157 | // Skip level indicator for cleaner output
158 | // We can still color the message based on level
159 | if (this.config.colors) {
160 | switch (level) {
161 | case LogLevel.ERROR:
162 | message = chalk.red(message);
163 | break;
164 | case LogLevel.WARN:
165 | message = chalk.yellow(message);
166 | break;
167 | case LogLevel.INFO:
168 | // Info stays default color
169 | break;
170 | case LogLevel.DEBUG:
171 | message = chalk.gray(message);
172 | break;
173 | }
174 | }
175 |
176 | // Add the message
177 | formatted += message;
178 |
179 | // Add any additional arguments
180 | if (args.length > 0) {
181 | formatted +=
182 | ' ' +
183 | args
184 | .map((arg) =>
185 | typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
186 | )
187 | .join(' ');
188 | }
189 |
190 | return formatted;
191 | }
192 |
193 | /**
194 | * Check if callback is a log object (has info/warn/error/debug methods)
195 | */
196 | private isLogObject(callback: LogCallback): callback is LogObject {
197 | return (
198 | typeof callback === 'object' &&
199 | callback !== null &&
200 | 'info' in callback &&
201 | 'warn' in callback &&
202 | 'error' in callback &&
203 | 'debug' in callback
204 | );
205 | }
206 |
207 | /**
208 | * Output a log message either to console or callback
209 | */
210 | private output(
211 | level: LogLevel,
212 | levelName: string,
213 | message: string,
214 | ...args: any[]
215 | ): void {
216 | const formatted = this.formatMessage(level, message, ...args);
217 |
218 | // Use callback if available
219 | if (this.config.logCallback) {
220 | // If callback is a log object, call the appropriate method
221 | if (this.isLogObject(this.config.logCallback)) {
222 | const method = levelName.toLowerCase() as keyof LogObject;
223 | if (method in this.config.logCallback) {
224 | this.config.logCallback[method](formatted);
225 | }
226 | } else {
227 | // Otherwise it's a function callback
228 | this.config.logCallback(levelName.toLowerCase(), formatted);
229 | }
230 | return;
231 | }
232 |
233 | // Otherwise use console
234 | switch (level) {
235 | case LogLevel.ERROR:
236 | console.error(formatted);
237 | break;
238 | case LogLevel.WARN:
239 | console.warn(formatted);
240 | break;
241 | default:
242 | console.log(formatted);
243 | break;
244 | }
245 | }
246 |
247 | /**
248 | * Log an error message
249 | */
250 | error(message: string, ...args: any[]): void {
251 | if (!this.shouldLog(LogLevel.ERROR)) return;
252 | this.output(LogLevel.ERROR, 'ERROR', message, ...args);
253 | }
254 |
255 | /**
256 | * Log a warning message
257 | */
258 | warn(message: string, ...args: any[]): void {
259 | if (!this.shouldLog(LogLevel.WARN)) return;
260 | this.output(LogLevel.WARN, 'WARN', message, ...args);
261 | }
262 |
263 | /**
264 | * Log an info message
265 | */
266 | info(message: string, ...args: any[]): void {
267 | if (!this.shouldLog(LogLevel.INFO)) return;
268 | this.output(LogLevel.INFO, 'INFO', message, ...args);
269 | }
270 |
271 | /**
272 | * Log a debug message
273 | */
274 | debug(message: string, ...args: any[]): void {
275 | if (!this.shouldLog(LogLevel.DEBUG)) return;
276 | this.output(LogLevel.DEBUG, 'DEBUG', message, ...args);
277 | }
278 |
279 | /**
280 | * Log a message without any formatting (raw output)
281 | * Useful for CLI output that should appear as-is
282 | */
283 | log(message: string, ...args: any[]): void {
284 | // If callback is provided, use it for raw logs too
285 | if (this.config.logCallback) {
286 | const fullMessage =
287 | args.length > 0 ? [message, ...args].join(' ') : message;
288 |
289 | // If callback is a log object, use info method for raw logs
290 | if (this.isLogObject(this.config.logCallback)) {
291 | this.config.logCallback.info(fullMessage);
292 | } else {
293 | // Otherwise it's a function callback
294 | this.config.logCallback('log', fullMessage);
295 | }
296 | return;
297 | }
298 |
299 | // Otherwise, respect silent/mcpMode
300 | if (this.config.silent || this.config.mcpMode) return;
301 |
302 | if (args.length > 0) {
303 | console.log(message, ...args);
304 | } else {
305 | console.log(message);
306 | }
307 | }
308 |
309 | /**
310 | * Update logger configuration
311 | */
312 | setConfig(config: Partial<LoggerConfig>): void {
313 | this.config = {
314 | ...this.config,
315 | ...config
316 | };
317 |
318 | // MCP mode overrides to silent ONLY if no callback is provided
319 | if (this.config.mcpMode && !this.config.logCallback) {
320 | this.config.silent = true;
321 | }
322 | }
323 |
324 | /**
325 | * Get current configuration
326 | */
327 | getConfig(): Readonly<
328 | LoggerConfig & {
329 | level: LogLevel;
330 | silent: boolean;
331 | prefix: string;
332 | timestamp: boolean;
333 | colors: boolean;
334 | mcpMode: boolean;
335 | }
336 | > {
337 | return { ...this.config };
338 | }
339 |
340 | /**
341 | * Create a child logger with a prefix
342 | */
343 | child(prefix: string, config?: Partial<LoggerConfig>): Logger {
344 | const childPrefix = this.config.prefix
345 | ? `${this.config.prefix}:${prefix}`
346 | : prefix;
347 |
348 | return new Logger({
349 | ...this.config,
350 | ...config,
351 | prefix: childPrefix
352 | });
353 | }
354 | }
355 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/workflow/managers/workflow-state-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview WorkflowStateManager - Manages persistence of TDD workflow state
3 | *
4 | * Stores workflow state in global user directory (~/.taskmaster/{project-id}/sessions/)
5 | * to avoid git conflicts and support multiple worktrees.
6 | * Each project gets its own directory for organizing workflow-related data.
7 | */
8 |
9 | import fs from 'node:fs/promises';
10 | import os from 'node:os';
11 | import path from 'node:path';
12 | import { Writer } from 'steno';
13 | import { getLogger } from '../../../common/logger/index.js';
14 | import type { WorkflowState } from '../types.js';
15 |
16 | export interface WorkflowStateBackup {
17 | timestamp: string;
18 | state: WorkflowState;
19 | }
20 |
21 | /**
22 | * Manages workflow state persistence with backup support
23 | * Stores state in global user directory to avoid git noise
24 | */
25 | export class WorkflowStateManager {
26 | private readonly projectRoot: string;
27 | private readonly statePath: string;
28 | private readonly backupDir: string;
29 | private readonly sessionDir: string;
30 | private maxBackups: number;
31 | private readonly logger = getLogger('WorkflowStateManager');
32 | private writer: Writer | null = null;
33 | private writerInitPromise: Promise<void> | null = null;
34 |
35 | constructor(projectRoot: string, maxBackups = 5) {
36 | this.projectRoot = path.resolve(projectRoot);
37 | this.maxBackups = maxBackups;
38 |
39 | // Create project-specific directory in global .taskmaster
40 | // Structure: ~/.taskmaster/{project-id}/sessions/
41 | const projectId = this.getProjectIdentifier(this.projectRoot);
42 | const homeDir = os.homedir();
43 | const projectDir = path.join(homeDir, '.taskmaster', projectId);
44 | this.sessionDir = path.join(projectDir, 'sessions');
45 |
46 | this.statePath = path.join(this.sessionDir, 'workflow-state.json');
47 | this.backupDir = path.join(this.sessionDir, 'backups');
48 | }
49 |
50 | /**
51 | * Generate a unique identifier for the project using full sanitized path
52 | * Uses Claude Code's pattern: leading dash + full path with case preserved
53 | * Example: /Volumes/Workspace/... -> -Volumes-Workspace-...
54 | */
55 | private getProjectIdentifier(projectRoot: string): string {
56 | // Resolve to absolute path
57 | const absolutePath = path.resolve(projectRoot);
58 |
59 | // Sanitize path like Claude Code does:
60 | // - Add leading dash
61 | // - Replace path separators and non-alphanumeric chars with dashes
62 | // - Preserve case for readability
63 | // - Collapse multiple dashes
64 | const sanitized =
65 | '-' +
66 | absolutePath
67 | .replace(/^\//, '') // Remove leading slash before adding dash
68 | .replace(/[^a-zA-Z0-9]+/g, '-') // Replace sequences of non-alphanumeric with single dash
69 | .replace(/-+/g, '-') // Collapse multiple dashes
70 | .replace(/-+$/, ''); // Remove trailing dashes
71 |
72 | return sanitized;
73 | }
74 |
75 | /**
76 | * Ensure the steno Writer is initialized
77 | * This ensures the session directory exists before creating the writer
78 | */
79 | private async ensureWriter(): Promise<void> {
80 | if (this.writer) {
81 | return;
82 | }
83 |
84 | // If another call is already initializing, wait for it
85 | if (this.writerInitPromise) {
86 | await this.writerInitPromise;
87 | return;
88 | }
89 |
90 | this.writerInitPromise = (async () => {
91 | // Ensure session directory exists before creating writer
92 | await fs.mkdir(this.sessionDir, { recursive: true });
93 | this.writer = new Writer(this.statePath);
94 | })();
95 |
96 | await this.writerInitPromise;
97 | this.writerInitPromise = null;
98 | }
99 |
100 | /**
101 | * Check if workflow state exists
102 | */
103 | async exists(): Promise<boolean> {
104 | try {
105 | await fs.access(this.statePath);
106 | return true;
107 | } catch {
108 | return false;
109 | }
110 | }
111 |
112 | /**
113 | * Load workflow state from disk
114 | */
115 | async load(): Promise<WorkflowState> {
116 | try {
117 | const content = await fs.readFile(this.statePath, 'utf-8');
118 | return JSON.parse(content) as WorkflowState;
119 | } catch (error: any) {
120 | if (error.code === 'ENOENT') {
121 | throw new Error(`Workflow state file not found at ${this.statePath}`);
122 | }
123 | throw new Error(`Failed to load workflow state: ${error.message}`);
124 | }
125 | }
126 |
127 | /**
128 | * Save workflow state to disk
129 | * Uses steno for atomic writes and automatic queueing of concurrent saves
130 | */
131 | async save(state: WorkflowState): Promise<void> {
132 | try {
133 | // Ensure writer is initialized (creates directory if needed)
134 | await this.ensureWriter();
135 |
136 | // Serialize and validate JSON
137 | const jsonContent = JSON.stringify(state, null, 2);
138 |
139 | // Validate that the JSON is well-formed by parsing it back
140 | try {
141 | JSON.parse(jsonContent);
142 | } catch (parseError) {
143 | this.logger.error('Generated invalid JSON:', jsonContent);
144 | throw new Error('Failed to generate valid JSON from workflow state');
145 | }
146 |
147 | // Write using steno (handles queuing and atomic writes automatically)
148 | await this.writer!.write(jsonContent + '\n');
149 |
150 | this.logger.debug(`Saved workflow state (${jsonContent.length} bytes)`);
151 | } catch (error: any) {
152 | throw new Error(`Failed to save workflow state: ${error.message}`);
153 | }
154 | }
155 |
156 | /**
157 | * Create a backup of current state
158 | */
159 | async createBackup(): Promise<void> {
160 | try {
161 | const exists = await this.exists();
162 | if (!exists) {
163 | return;
164 | }
165 |
166 | const state = await this.load();
167 | await fs.mkdir(this.backupDir, { recursive: true });
168 |
169 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
170 | const backupPath = path.join(
171 | this.backupDir,
172 | `workflow-state-${timestamp}.json`
173 | );
174 |
175 | const backup: WorkflowStateBackup = {
176 | timestamp: new Date().toISOString(),
177 | state
178 | };
179 |
180 | await fs.writeFile(backupPath, JSON.stringify(backup, null, 2), 'utf-8');
181 |
182 | // Clean up old backups
183 | await this.pruneBackups();
184 | } catch (error: any) {
185 | throw new Error(`Failed to create backup: ${error.message}`);
186 | }
187 | }
188 |
189 | /**
190 | * Delete workflow state file
191 | */
192 | async delete(): Promise<void> {
193 | try {
194 | await fs.unlink(this.statePath);
195 | } catch (error: any) {
196 | if (error.code !== 'ENOENT') {
197 | throw new Error(`Failed to delete workflow state: ${error.message}`);
198 | }
199 | }
200 | }
201 |
202 | /**
203 | * List available backups
204 | */
205 | async listBackups(): Promise<string[]> {
206 | try {
207 | const files = await fs.readdir(this.backupDir);
208 | return files
209 | .filter((f) => f.startsWith('workflow-state-') && f.endsWith('.json'))
210 | .sort()
211 | .reverse();
212 | } catch (error: any) {
213 | if (error.code === 'ENOENT') {
214 | return [];
215 | }
216 | throw new Error(`Failed to list backups: ${error.message}`);
217 | }
218 | }
219 |
220 | /**
221 | * Restore from a backup
222 | */
223 | async restoreBackup(backupFileName: string): Promise<void> {
224 | try {
225 | const backupPath = path.join(this.backupDir, backupFileName);
226 | const content = await fs.readFile(backupPath, 'utf-8');
227 | const backup: WorkflowStateBackup = JSON.parse(content);
228 |
229 | await this.save(backup.state);
230 | } catch (error: any) {
231 | throw new Error(`Failed to restore backup: ${error.message}`);
232 | }
233 | }
234 |
235 | /**
236 | * Prune old backups to maintain max backup count
237 | */
238 | private async pruneBackups(): Promise<void> {
239 | try {
240 | const backups = await this.listBackups();
241 |
242 | if (backups.length > this.maxBackups) {
243 | const toDelete = backups.slice(this.maxBackups);
244 |
245 | for (const backup of toDelete) {
246 | await fs.unlink(path.join(this.backupDir, backup));
247 | }
248 | }
249 | } catch (error: any) {
250 | // Non-critical error, log but don't throw
251 | this.logger.warn(`Failed to prune backups: ${error.message}`);
252 | }
253 | }
254 |
255 | /**
256 | * Get the path to the state file (for debugging/testing)
257 | */
258 | getStatePath(): string {
259 | return this.statePath;
260 | }
261 |
262 | /**
263 | * Get the path to the backup directory (for debugging/testing)
264 | */
265 | getBackupDir(): string {
266 | return this.backupDir;
267 | }
268 |
269 | /**
270 | * Get the session directory path (for debugging/testing)
271 | */
272 | getSessionDir(): string {
273 | return this.sessionDir;
274 | }
275 |
276 | /**
277 | * Get the project root this manager is for
278 | */
279 | getProjectRoot(): string {
280 | return this.projectRoot;
281 | }
282 |
283 | /**
284 | * Get the path to the activity log file
285 | * Activity log is stored next to workflow-state.json for correlation
286 | */
287 | getActivityLogPath(): string {
288 | return path.join(this.sessionDir, 'activity.jsonl');
289 | }
290 | }
291 |
```
--------------------------------------------------------------------------------
/tests/unit/mcp/tools/move-task-cross-tag.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 |
3 | // Mock the utils functions
4 | const mockFindTasksPath = jest
5 | .fn()
6 | .mockReturnValue('/test/path/.taskmaster/tasks/tasks.json');
7 | jest.mock('../../../../mcp-server/src/core/utils/path-utils.js', () => ({
8 | findTasksPath: mockFindTasksPath
9 | }));
10 |
11 | const mockEnableSilentMode = jest.fn();
12 | const mockDisableSilentMode = jest.fn();
13 | const mockReadJSON = jest.fn();
14 | const mockWriteJSON = jest.fn();
15 | jest.mock('../../../../scripts/modules/utils.js', () => ({
16 | enableSilentMode: mockEnableSilentMode,
17 | disableSilentMode: mockDisableSilentMode,
18 | readJSON: mockReadJSON,
19 | writeJSON: mockWriteJSON
20 | }));
21 |
22 | // Import the direct function after setting up mocks
23 | import { moveTaskCrossTagDirect } from '../../../../mcp-server/src/core/direct-functions/move-task-cross-tag.js';
24 |
25 | describe('MCP Cross-Tag Move Direct Function', () => {
26 | const mockLog = {
27 | info: jest.fn(),
28 | error: jest.fn(),
29 | warn: jest.fn()
30 | };
31 |
32 | beforeEach(() => {
33 | jest.clearAllMocks();
34 | });
35 |
36 | describe('Mock Verification', () => {
37 | it('should verify that mocks are working', () => {
38 | // Test that findTasksPath mock is working
39 | expect(mockFindTasksPath()).toBe(
40 | '/test/path/.taskmaster/tasks/tasks.json'
41 | );
42 |
43 | // Test that readJSON mock is working
44 | mockReadJSON.mockReturnValue('test');
45 | expect(mockReadJSON()).toBe('test');
46 | });
47 | });
48 |
49 | describe('Parameter Validation', () => {
50 | it('should return error when source IDs are missing', async () => {
51 | const result = await moveTaskCrossTagDirect(
52 | {
53 | sourceTag: 'backlog',
54 | targetTag: 'in-progress',
55 | projectRoot: '/test'
56 | },
57 | mockLog
58 | );
59 |
60 | expect(result.success).toBe(false);
61 | expect(result.error.code).toBe('MISSING_SOURCE_IDS');
62 | expect(result.error.message).toBe('Source IDs are required');
63 | });
64 |
65 | it('should return error when source tag is missing', async () => {
66 | const result = await moveTaskCrossTagDirect(
67 | {
68 | sourceIds: '1,2',
69 | targetTag: 'in-progress',
70 | projectRoot: '/test'
71 | },
72 | mockLog
73 | );
74 |
75 | expect(result.success).toBe(false);
76 | expect(result.error.code).toBe('MISSING_SOURCE_TAG');
77 | expect(result.error.message).toBe(
78 | 'Source tag is required for cross-tag moves'
79 | );
80 | });
81 |
82 | it('should return error when target tag is missing', async () => {
83 | const result = await moveTaskCrossTagDirect(
84 | {
85 | sourceIds: '1,2',
86 | sourceTag: 'backlog',
87 | projectRoot: '/test'
88 | },
89 | mockLog
90 | );
91 |
92 | expect(result.success).toBe(false);
93 | expect(result.error.code).toBe('MISSING_TARGET_TAG');
94 | expect(result.error.message).toBe(
95 | 'Target tag is required for cross-tag moves'
96 | );
97 | });
98 |
99 | it('should return error when source and target tags are the same', async () => {
100 | const result = await moveTaskCrossTagDirect(
101 | {
102 | sourceIds: '1,2',
103 | sourceTag: 'backlog',
104 | targetTag: 'backlog',
105 | projectRoot: '/test'
106 | },
107 | mockLog
108 | );
109 |
110 | expect(result.success).toBe(false);
111 | expect(result.error.code).toBe('SAME_SOURCE_TARGET_TAG');
112 | expect(result.error.message).toBe(
113 | 'Source and target tags are the same ("backlog")'
114 | );
115 | expect(result.error.suggestions).toHaveLength(3);
116 | });
117 | });
118 |
119 | describe('Error Code Mapping', () => {
120 | it('should map tag not found errors correctly', async () => {
121 | const result = await moveTaskCrossTagDirect(
122 | {
123 | sourceIds: '1',
124 | sourceTag: 'invalid',
125 | targetTag: 'in-progress',
126 | projectRoot: '/test'
127 | },
128 | mockLog
129 | );
130 |
131 | expect(result.success).toBe(false);
132 | expect(result.error.code).toBe('TAG_OR_TASK_NOT_FOUND');
133 | expect(result.error.message).toBe(
134 | 'Source tag "invalid" not found or invalid'
135 | );
136 | expect(result.error.suggestions).toHaveLength(3);
137 | });
138 |
139 | it('should map missing project root errors correctly', async () => {
140 | const result = await moveTaskCrossTagDirect(
141 | {
142 | sourceIds: '1',
143 | sourceTag: 'backlog',
144 | targetTag: 'in-progress'
145 | // Missing projectRoot
146 | },
147 | mockLog
148 | );
149 |
150 | expect(result.success).toBe(false);
151 | expect(result.error.code).toBe('MISSING_PROJECT_ROOT');
152 | expect(result.error.message).toBe(
153 | 'Project root is required if tasksJsonPath is not provided'
154 | );
155 | });
156 | });
157 |
158 | describe('Move Options Handling', () => {
159 | it('should handle move options correctly', async () => {
160 | const result = await moveTaskCrossTagDirect(
161 | {
162 | sourceIds: '1',
163 | sourceTag: 'backlog',
164 | targetTag: 'in-progress',
165 | withDependencies: true,
166 | ignoreDependencies: false,
167 | projectRoot: '/test'
168 | },
169 | mockLog
170 | );
171 |
172 | // The function should fail due to missing tag, but options should be processed
173 | expect(result.success).toBe(false);
174 | expect(result.error.code).toBe('TAG_OR_TASK_NOT_FOUND');
175 | });
176 | });
177 |
178 | describe('Function Call Flow', () => {
179 | it('should call findTasksPath when projectRoot is provided', async () => {
180 | const result = await moveTaskCrossTagDirect(
181 | {
182 | sourceIds: '1',
183 | sourceTag: 'backlog',
184 | targetTag: 'in-progress',
185 | projectRoot: '/test'
186 | },
187 | mockLog
188 | );
189 |
190 | // The function should fail due to tag validation before reaching path resolution
191 | expect(result.success).toBe(false);
192 | expect(result.error.code).toBe('TAG_OR_TASK_NOT_FOUND');
193 |
194 | // Since the function fails early, findTasksPath is not called
195 | expect(mockFindTasksPath).toHaveBeenCalledTimes(0);
196 | });
197 |
198 | it('should enable and disable silent mode during execution', async () => {
199 | const result = await moveTaskCrossTagDirect(
200 | {
201 | sourceIds: '1',
202 | sourceTag: 'backlog',
203 | targetTag: 'in-progress',
204 | projectRoot: '/test'
205 | },
206 | mockLog
207 | );
208 |
209 | // The function should fail due to tag validation before reaching silent mode calls
210 | expect(result.success).toBe(false);
211 | expect(result.error.code).toBe('TAG_OR_TASK_NOT_FOUND');
212 |
213 | // Since the function fails early, silent mode is not called
214 | expect(mockEnableSilentMode).toHaveBeenCalledTimes(0);
215 | expect(mockDisableSilentMode).toHaveBeenCalledTimes(0);
216 | });
217 |
218 | it('should parse source IDs correctly', async () => {
219 | const result = await moveTaskCrossTagDirect(
220 | {
221 | sourceIds: '1, 2, 3', // With spaces
222 | sourceTag: 'backlog',
223 | targetTag: 'in-progress',
224 | projectRoot: '/test'
225 | },
226 | mockLog
227 | );
228 |
229 | // Should fail due to tag validation, but ID parsing should work
230 | expect(result.success).toBe(false);
231 | expect(result.error.code).toBe('TAG_OR_TASK_NOT_FOUND');
232 | });
233 |
234 | it('should handle move options correctly', async () => {
235 | const result = await moveTaskCrossTagDirect(
236 | {
237 | sourceIds: '1',
238 | sourceTag: 'backlog',
239 | targetTag: 'in-progress',
240 | withDependencies: true,
241 | ignoreDependencies: false,
242 | projectRoot: '/test'
243 | },
244 | mockLog
245 | );
246 |
247 | // Should fail due to tag validation, but option processing should work
248 | expect(result.success).toBe(false);
249 | expect(result.error.code).toBe('TAG_OR_TASK_NOT_FOUND');
250 | });
251 | });
252 |
253 | describe('Error Handling', () => {
254 | it('should handle missing project root correctly', async () => {
255 | const result = await moveTaskCrossTagDirect(
256 | {
257 | sourceIds: '1',
258 | sourceTag: 'backlog',
259 | targetTag: 'in-progress'
260 | // Missing projectRoot
261 | },
262 | mockLog
263 | );
264 |
265 | expect(result.success).toBe(false);
266 | expect(result.error.code).toBe('MISSING_PROJECT_ROOT');
267 | expect(result.error.message).toBe(
268 | 'Project root is required if tasksJsonPath is not provided'
269 | );
270 | });
271 |
272 | it('should handle same source and target tags', async () => {
273 | const result = await moveTaskCrossTagDirect(
274 | {
275 | sourceIds: '1',
276 | sourceTag: 'backlog',
277 | targetTag: 'backlog',
278 | projectRoot: '/test'
279 | },
280 | mockLog
281 | );
282 |
283 | expect(result.success).toBe(false);
284 | expect(result.error.code).toBe('SAME_SOURCE_TARGET_TAG');
285 | expect(result.error.message).toBe(
286 | 'Source and target tags are the same ("backlog")'
287 | );
288 | expect(result.error.suggestions).toHaveLength(3);
289 | });
290 | });
291 | });
292 |
```
--------------------------------------------------------------------------------
/apps/cli/src/utils/brief-selection.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Shared brief selection utilities
3 | * Reusable functions for selecting briefs interactively or via URL/ID
4 | */
5 |
6 | import search from '@inquirer/search';
7 | import type { AuthManager } from '@tm/core';
8 | import { formatRelativeTime } from '@tm/core';
9 | import chalk from 'chalk';
10 | import ora, { type Ora } from 'ora';
11 | import { getBriefStatusWithColor } from '../ui/formatters/status-formatters.js';
12 | import * as ui from './ui.js';
13 |
14 | export interface BriefSelectionResult {
15 | success: boolean;
16 | briefId?: string;
17 | briefName?: string;
18 | orgId?: string;
19 | orgName?: string;
20 | message?: string;
21 | }
22 |
23 | /**
24 | * Select a brief interactively using search
25 | */
26 | export async function selectBriefInteractive(
27 | authManager: AuthManager,
28 | orgId: string
29 | ): Promise<BriefSelectionResult> {
30 | const spinner = ora('Fetching briefs...').start();
31 |
32 | try {
33 | // Fetch briefs from API
34 | const briefs = await authManager.getBriefs(orgId);
35 | spinner.stop();
36 |
37 | if (briefs.length === 0) {
38 | ui.displayWarning('No briefs available in this organization');
39 | return {
40 | success: false,
41 | message: 'No briefs available'
42 | };
43 | }
44 |
45 | // Prompt for selection with search
46 | const selectedBrief = await search<(typeof briefs)[0] | null>({
47 | message: 'Search for a brief:',
48 | source: async (input) => {
49 | const searchTerm = input?.toLowerCase() || '';
50 |
51 | // Static option for no brief
52 | const noBriefOption = {
53 | name: '(No brief - organization level)',
54 | value: null as any,
55 | description: 'Clear brief selection'
56 | };
57 |
58 | // Filter briefs based on search term
59 | const filteredBriefs = briefs.filter((brief) => {
60 | if (!searchTerm) return true;
61 |
62 | const title = brief.document?.title || '';
63 | const shortId = brief.id.slice(0, 8);
64 | const lastChars = brief.id.slice(-8);
65 |
66 | // Search by title, full UUID, first 8 chars, or last 8 chars
67 | return (
68 | title.toLowerCase().includes(searchTerm) ||
69 | brief.id.toLowerCase().includes(searchTerm) ||
70 | shortId.toLowerCase().includes(searchTerm) ||
71 | lastChars.toLowerCase().includes(searchTerm)
72 | );
73 | });
74 |
75 | // Group briefs by status
76 | const briefsByStatus = filteredBriefs.reduce(
77 | (acc, brief) => {
78 | const status = brief.status || 'unknown';
79 | if (!acc[status]) {
80 | acc[status] = [];
81 | }
82 | acc[status].push(brief);
83 | return acc;
84 | },
85 | {} as Record<string, typeof briefs>
86 | );
87 |
88 | // Define status order (most active first)
89 | const statusOrder = [
90 | 'delivering',
91 | 'aligned',
92 | 'refining',
93 | 'draft',
94 | 'delivered',
95 | 'done',
96 | 'archived'
97 | ];
98 |
99 | // Build grouped options
100 | const groupedOptions: any[] = [];
101 |
102 | for (const status of statusOrder) {
103 | const statusBriefs = briefsByStatus[status];
104 | if (!statusBriefs || statusBriefs.length === 0) continue;
105 |
106 | // Add status header as separator
107 | const statusHeader = getBriefStatusWithColor(status);
108 | groupedOptions.push({
109 | type: 'separator',
110 | separator: `\n${statusHeader}`
111 | });
112 |
113 | // Add briefs under this status
114 | statusBriefs.forEach((brief) => {
115 | const title =
116 | brief.document?.title || `Brief ${brief.id.slice(-8)}`;
117 | const shortId = brief.id.slice(-8);
118 | const description = brief.document?.description || '';
119 | const taskCountDisplay =
120 | brief.taskCount !== undefined && brief.taskCount > 0
121 | ? chalk.gray(
122 | ` (${brief.taskCount} ${brief.taskCount === 1 ? 'task' : 'tasks'})`
123 | )
124 | : '';
125 |
126 | const updatedAtDisplay = brief.updatedAt
127 | ? chalk.gray(` • ${formatRelativeTime(brief.updatedAt)}`)
128 | : '';
129 |
130 | groupedOptions.push({
131 | name: ` ${title}${taskCountDisplay} ${chalk.gray(`(${shortId})`)}${updatedAtDisplay}`,
132 | value: brief,
133 | description: description
134 | ? chalk.gray(` ${description.slice(0, 80)}`)
135 | : undefined
136 | });
137 | });
138 | }
139 |
140 | // Handle any briefs with statuses not in our order
141 | const unorderedStatuses = Object.keys(briefsByStatus).filter(
142 | (s) => !statusOrder.includes(s)
143 | );
144 | for (const status of unorderedStatuses) {
145 | const statusBriefs = briefsByStatus[status];
146 | if (!statusBriefs || statusBriefs.length === 0) continue;
147 |
148 | const statusHeader = getBriefStatusWithColor(status);
149 | groupedOptions.push({
150 | type: 'separator',
151 | separator: `\n${statusHeader}`
152 | });
153 |
154 | statusBriefs.forEach((brief) => {
155 | const title =
156 | brief.document?.title || `Brief ${brief.id.slice(-8)}`;
157 | const shortId = brief.id.slice(-8);
158 | const description = brief.document?.description || '';
159 | const taskCountDisplay =
160 | brief.taskCount !== undefined && brief.taskCount > 0
161 | ? chalk.gray(
162 | ` (${brief.taskCount} ${brief.taskCount === 1 ? 'task' : 'tasks'})`
163 | )
164 | : '';
165 |
166 | const updatedAtDisplay = brief.updatedAt
167 | ? chalk.gray(` • ${formatRelativeTime(brief.updatedAt)}`)
168 | : '';
169 |
170 | groupedOptions.push({
171 | name: ` ${title}${taskCountDisplay} ${chalk.gray(`(${shortId})`)}${updatedAtDisplay}`,
172 | value: brief,
173 | description: description
174 | ? chalk.gray(` ${description.slice(0, 80)}`)
175 | : undefined
176 | });
177 | });
178 | }
179 |
180 | return [noBriefOption, ...groupedOptions];
181 | }
182 | });
183 |
184 | if (selectedBrief) {
185 | // Update context with brief
186 | const briefName =
187 | selectedBrief.document?.title ||
188 | `Brief ${selectedBrief.id.slice(0, 8)}`;
189 | await authManager.updateContext({
190 | briefId: selectedBrief.id,
191 | briefName: briefName,
192 | briefStatus: selectedBrief.status,
193 | briefUpdatedAt: selectedBrief.updatedAt
194 | });
195 |
196 | ui.displaySuccess(`Selected brief: ${briefName}`);
197 |
198 | return {
199 | success: true,
200 | briefId: selectedBrief.id,
201 | briefName,
202 | message: `Selected brief: ${briefName}`
203 | };
204 | } else {
205 | // Clear brief selection
206 | await authManager.updateContext({
207 | briefId: undefined,
208 | briefName: undefined,
209 | briefStatus: undefined,
210 | briefUpdatedAt: undefined
211 | });
212 |
213 | ui.displaySuccess('Cleared brief selection (organization level)');
214 |
215 | return {
216 | success: true,
217 | message: 'Cleared brief selection'
218 | };
219 | }
220 | } catch (error) {
221 | spinner.fail('Failed to fetch briefs');
222 | throw error;
223 | }
224 | }
225 |
226 | /**
227 | * Select a brief from any input format (URL, ID, name) using tm-core
228 | * Presentation layer - handles display and context updates only
229 | *
230 | * All business logic (URL parsing, ID matching, name resolution) is in tm-core
231 | */
232 | export async function selectBriefFromInput(
233 | authManager: AuthManager,
234 | input: string,
235 | tmCore: any
236 | ): Promise<BriefSelectionResult> {
237 | let spinner: Ora | undefined;
238 | try {
239 | spinner = ora('Resolving brief...');
240 | spinner.start();
241 |
242 | // Let tm-core handle ALL business logic:
243 | // - URL parsing
244 | // - ID extraction
245 | // - UUID matching (full or last 8 chars)
246 | // - Name matching
247 | const brief = await tmCore.tasks.resolveBrief(input);
248 |
249 | // Fetch org to get a friendly name and slug (optional)
250 | let orgName: string | undefined;
251 | let orgSlug: string | undefined;
252 | try {
253 | const org = await authManager.getOrganization(brief.accountId);
254 | orgName = org?.name;
255 | orgSlug = org?.slug;
256 | } catch {
257 | // Non-fatal if org lookup fails
258 | }
259 |
260 | // Update context: set org and brief
261 | const briefName = brief.document?.title || `Brief ${brief.id.slice(0, 8)}`;
262 | await authManager.updateContext({
263 | orgId: brief.accountId,
264 | orgName,
265 | orgSlug,
266 | briefId: brief.id,
267 | briefName,
268 | briefStatus: brief.status,
269 | briefUpdatedAt: brief.updatedAt
270 | });
271 |
272 | spinner.succeed('Context set from brief');
273 | console.log(
274 | chalk.gray(
275 | ` Organization: ${orgName || brief.accountId}\n Brief: ${briefName}`
276 | )
277 | );
278 |
279 | return {
280 | success: true,
281 | briefId: brief.id,
282 | briefName,
283 | orgId: brief.accountId,
284 | orgName,
285 | message: 'Context set from brief'
286 | };
287 | } catch (error) {
288 | try {
289 | if (spinner?.isSpinning) spinner.stop();
290 | } catch {}
291 | throw error;
292 | }
293 | }
294 |
```
--------------------------------------------------------------------------------
/scripts/modules/task-manager/migrate.js:
--------------------------------------------------------------------------------
```javascript
1 | import fs from 'fs';
2 | import path from 'path';
3 | import chalk from 'chalk';
4 | import { fileURLToPath } from 'url';
5 | import { createLogWrapper } from '../../../mcp-server/src/tools/utils.js';
6 | import { findProjectRoot } from '../utils.js';
7 | import {
8 | LEGACY_CONFIG_FILE,
9 | TASKMASTER_CONFIG_FILE
10 | } from '../../../src/constants/paths.js';
11 |
12 | const __filename = fileURLToPath(import.meta.url);
13 | const __dirname = path.dirname(__filename);
14 |
15 | // Create a simple log wrapper for CLI use
16 | const log = createLogWrapper({
17 | info: (msg) => console.log(chalk.blue('ℹ'), msg),
18 | warn: (msg) => console.log(chalk.yellow('⚠'), msg),
19 | error: (msg) => console.error(chalk.red('✗'), msg),
20 | success: (msg) => console.log(chalk.green('✓'), msg)
21 | });
22 |
23 | /**
24 | * Main migration function
25 | * @param {Object} options - Migration options
26 | */
27 | export async function migrateProject(options = {}) {
28 | const projectRoot = findProjectRoot() || process.cwd();
29 |
30 | log.info(`Starting migration in: ${projectRoot}`);
31 |
32 | // Check if .taskmaster directory already exists
33 | const taskmasterDir = path.join(projectRoot, '.taskmaster');
34 | if (fs.existsSync(taskmasterDir) && !options.force) {
35 | log.warn(
36 | '.taskmaster directory already exists. Use --force to overwrite or skip migration.'
37 | );
38 | return;
39 | }
40 |
41 | // Analyze what needs to be migrated
42 | const migrationPlan = analyzeMigrationNeeds(projectRoot);
43 |
44 | if (migrationPlan.length === 0) {
45 | log.info(
46 | 'No files to migrate. Project may already be using the new structure.'
47 | );
48 | return;
49 | }
50 |
51 | // Show migration plan
52 | log.info('Migration plan:');
53 | for (const item of migrationPlan) {
54 | const action = options.dryRun ? 'Would move' : 'Will move';
55 | log.info(` ${action}: ${item.from} → ${item.to}`);
56 | }
57 |
58 | if (options.dryRun) {
59 | log.info(
60 | 'Dry run complete. Use --dry-run=false to perform actual migration.'
61 | );
62 | return;
63 | }
64 |
65 | // Confirm migration
66 | if (!options.yes) {
67 | const readline = await import('readline');
68 | const rl = readline.createInterface({
69 | input: process.stdin,
70 | output: process.stdout
71 | });
72 |
73 | const answer = await new Promise((resolve) => {
74 | rl.question('Proceed with migration? (y/N): ', resolve);
75 | });
76 | rl.close();
77 |
78 | if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
79 | log.info('Migration cancelled.');
80 | return;
81 | }
82 | }
83 |
84 | // Perform migration
85 | try {
86 | await performMigration(projectRoot, migrationPlan, options);
87 | log.success('Migration completed successfully!');
88 | log.info('You can now use the new .taskmaster directory structure.');
89 | if (!options.cleanup) {
90 | log.info(
91 | 'Old files were preserved. Use --cleanup to remove them after verification.'
92 | );
93 | }
94 | } catch (error) {
95 | log.error(`Migration failed: ${error.message}`);
96 | throw error;
97 | }
98 | }
99 |
100 | /**
101 | * Analyze what files need to be migrated
102 | * @param {string} projectRoot - Project root directory
103 | * @returns {Array} Migration plan items
104 | */
105 | function analyzeMigrationNeeds(projectRoot) {
106 | const migrationPlan = [];
107 |
108 | // Check for tasks directory
109 | const tasksDir = path.join(projectRoot, 'tasks');
110 | if (fs.existsSync(tasksDir)) {
111 | const tasksFiles = fs.readdirSync(tasksDir);
112 | for (const file of tasksFiles) {
113 | migrationPlan.push({
114 | from: path.join('tasks', file),
115 | to: path.join('.taskmaster', 'tasks', file),
116 | type: 'task'
117 | });
118 | }
119 | }
120 |
121 | // Check for scripts directory files
122 | const scriptsDir = path.join(projectRoot, 'scripts');
123 | if (fs.existsSync(scriptsDir)) {
124 | const scriptsFiles = fs.readdirSync(scriptsDir);
125 | for (const file of scriptsFiles) {
126 | const filePath = path.join(scriptsDir, file);
127 | if (fs.statSync(filePath).isFile()) {
128 | // Categorize files more intelligently
129 | let destination;
130 | const lowerFile = file.toLowerCase();
131 |
132 | if (
133 | lowerFile.includes('example') ||
134 | lowerFile.includes('template') ||
135 | lowerFile.includes('boilerplate') ||
136 | lowerFile.includes('sample')
137 | ) {
138 | // Template/example files go to templates (including example_prd.txt)
139 | destination = path.join('.taskmaster', 'templates', file);
140 | } else if (
141 | lowerFile.includes('complexity') &&
142 | lowerFile.includes('report') &&
143 | lowerFile.endsWith('.json')
144 | ) {
145 | // Only actual complexity reports go to reports
146 | destination = path.join('.taskmaster', 'reports', file);
147 | } else if (
148 | lowerFile.includes('prd') ||
149 | lowerFile.endsWith('.md') ||
150 | lowerFile.endsWith('.txt')
151 | ) {
152 | // Documentation files go to docs (but not examples or reports)
153 | destination = path.join('.taskmaster', 'docs', file);
154 | } else {
155 | // Other files stay in scripts or get skipped - don't force everything into templates
156 | log.warn(
157 | `Skipping migration of '${file}' - uncertain categorization. You may need to move this manually.`
158 | );
159 | continue;
160 | }
161 |
162 | migrationPlan.push({
163 | from: path.join('scripts', file),
164 | to: destination,
165 | type: 'script'
166 | });
167 | }
168 | }
169 | }
170 |
171 | // Check for .taskmasterconfig
172 | const oldConfig = path.join(projectRoot, LEGACY_CONFIG_FILE);
173 | if (fs.existsSync(oldConfig)) {
174 | migrationPlan.push({
175 | from: LEGACY_CONFIG_FILE,
176 | to: TASKMASTER_CONFIG_FILE,
177 | type: 'config'
178 | });
179 | }
180 |
181 | return migrationPlan;
182 | }
183 |
184 | /**
185 | * Perform the actual migration
186 | * @param {string} projectRoot - Project root directory
187 | * @param {Array} migrationPlan - List of files to migrate
188 | * @param {Object} options - Migration options
189 | */
190 | async function performMigration(projectRoot, migrationPlan, options) {
191 | // Create .taskmaster directory
192 | const taskmasterDir = path.join(projectRoot, '.taskmaster');
193 | if (!fs.existsSync(taskmasterDir)) {
194 | fs.mkdirSync(taskmasterDir, { recursive: true });
195 | }
196 |
197 | // Group migration items by destination directory to create only needed subdirs
198 | const neededDirs = new Set();
199 | for (const item of migrationPlan) {
200 | const destDir = path.dirname(item.to);
201 | neededDirs.add(destDir);
202 | }
203 |
204 | // Create only the directories we actually need
205 | for (const dir of neededDirs) {
206 | const fullDirPath = path.join(projectRoot, dir);
207 | if (!fs.existsSync(fullDirPath)) {
208 | fs.mkdirSync(fullDirPath, { recursive: true });
209 | log.info(`Created directory: ${dir}`);
210 | }
211 | }
212 |
213 | // Create backup if requested
214 | if (options.backup) {
215 | const backupDir = path.join(projectRoot, '.taskmaster-migration-backup');
216 | log.info(`Creating backup in: ${backupDir}`);
217 | if (fs.existsSync(backupDir)) {
218 | fs.rmSync(backupDir, { recursive: true, force: true });
219 | }
220 | fs.mkdirSync(backupDir, { recursive: true });
221 | }
222 |
223 | // Migrate files
224 | for (const item of migrationPlan) {
225 | const fromPath = path.join(projectRoot, item.from);
226 | const toPath = path.join(projectRoot, item.to);
227 |
228 | if (!fs.existsSync(fromPath)) {
229 | log.warn(`Source file not found: ${item.from}`);
230 | continue;
231 | }
232 |
233 | // Create backup if requested
234 | if (options.backup) {
235 | const backupPath = path.join(
236 | projectRoot,
237 | '.taskmaster-migration-backup',
238 | item.from
239 | );
240 | const backupDir = path.dirname(backupPath);
241 | if (!fs.existsSync(backupDir)) {
242 | fs.mkdirSync(backupDir, { recursive: true });
243 | }
244 | fs.copyFileSync(fromPath, backupPath);
245 | }
246 |
247 | // Ensure destination directory exists
248 | const toDir = path.dirname(toPath);
249 | if (!fs.existsSync(toDir)) {
250 | fs.mkdirSync(toDir, { recursive: true });
251 | }
252 |
253 | // Copy file
254 | fs.copyFileSync(fromPath, toPath);
255 | log.info(`Migrated: ${item.from} → ${item.to}`);
256 |
257 | // Remove original if cleanup is requested
258 | if (options.cleanup) {
259 | fs.unlinkSync(fromPath);
260 | }
261 | }
262 |
263 | // Clean up empty directories if cleanup is requested
264 | if (options.cleanup) {
265 | const dirsToCheck = ['tasks', 'scripts'];
266 | for (const dir of dirsToCheck) {
267 | const dirPath = path.join(projectRoot, dir);
268 | if (fs.existsSync(dirPath)) {
269 | try {
270 | const files = fs.readdirSync(dirPath);
271 | if (files.length === 0) {
272 | fs.rmdirSync(dirPath);
273 | log.info(`Removed empty directory: ${dir}`);
274 | }
275 | } catch (error) {
276 | // Directory not empty or other error, skip
277 | }
278 | }
279 | }
280 | }
281 | }
282 |
283 | export default { migrateProject };
284 |
```
--------------------------------------------------------------------------------
/src/utils/create-mcp-config.js:
--------------------------------------------------------------------------------
```javascript
1 | import fs from 'fs';
2 | import path from 'path';
3 | import { log } from '../../scripts/modules/utils.js';
4 |
5 | // Return JSON with existing mcp.json formatting style
6 | function formatJSONWithTabs(obj) {
7 | let json = JSON.stringify(obj, null, '\t');
8 |
9 | json = json.replace(
10 | /(\[\n\t+)([^[\]]+?)(\n\t+\])/g,
11 | (match, openBracket, content, closeBracket) => {
12 | // Only convert to single line if content doesn't contain nested objects/arrays
13 | if (!content.includes('{') && !content.includes('[')) {
14 | const singleLineContent = content
15 | .replace(/\n\t+/g, ' ')
16 | .replace(/\s+/g, ' ')
17 | .trim();
18 | return `[${singleLineContent}]`;
19 | }
20 | return match;
21 | }
22 | );
23 |
24 | return json;
25 | }
26 |
27 | // Structure matches project conventions (see scripts/init.js)
28 | export function setupMCPConfiguration(projectRoot, mcpConfigPath) {
29 | // Handle null mcpConfigPath (e.g., for Claude/Codex profiles)
30 | if (!mcpConfigPath) {
31 | log(
32 | 'debug',
33 | '[MCP Config] No mcpConfigPath provided, skipping MCP configuration setup'
34 | );
35 | return;
36 | }
37 |
38 | // Build the full path to the MCP config file
39 | const mcpPath = path.join(projectRoot, mcpConfigPath);
40 | const configDir = path.dirname(mcpPath);
41 |
42 | log('info', `Setting up MCP configuration at ${mcpPath}...`);
43 |
44 | // New MCP config to be added - references the installed package
45 | const newMCPServer = {
46 | 'task-master-ai': {
47 | command: 'npx',
48 | args: ['-y', 'task-master-ai'],
49 | env: {
50 | ANTHROPIC_API_KEY: 'YOUR_ANTHROPIC_API_KEY_HERE',
51 | PERPLEXITY_API_KEY: 'YOUR_PERPLEXITY_API_KEY_HERE',
52 | OPENAI_API_KEY: 'YOUR_OPENAI_KEY_HERE',
53 | GOOGLE_API_KEY: 'YOUR_GOOGLE_KEY_HERE',
54 | XAI_API_KEY: 'YOUR_XAI_KEY_HERE',
55 | OPENROUTER_API_KEY: 'YOUR_OPENROUTER_KEY_HERE',
56 | MISTRAL_API_KEY: 'YOUR_MISTRAL_KEY_HERE',
57 | AZURE_OPENAI_API_KEY: 'YOUR_AZURE_KEY_HERE',
58 | OLLAMA_API_KEY: 'YOUR_OLLAMA_API_KEY_HERE'
59 | }
60 | }
61 | };
62 |
63 | // Create config directory if it doesn't exist
64 | if (!fs.existsSync(configDir)) {
65 | fs.mkdirSync(configDir, { recursive: true });
66 | }
67 |
68 | if (fs.existsSync(mcpPath)) {
69 | log(
70 | 'info',
71 | 'MCP configuration file already exists, checking for existing task-master-ai...'
72 | );
73 | try {
74 | // Read existing config
75 | const mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
76 | // Initialize mcpServers if it doesn't exist
77 | if (!mcpConfig.mcpServers) {
78 | mcpConfig.mcpServers = {};
79 | }
80 | // Check if any existing server configuration already has task-master-ai in its args
81 | const hasMCPString = Object.values(mcpConfig.mcpServers).some(
82 | (server) =>
83 | server.args &&
84 | Array.isArray(server.args) &&
85 | server.args.some(
86 | (arg) => typeof arg === 'string' && arg.includes('task-master-ai')
87 | )
88 | );
89 | if (hasMCPString) {
90 | log(
91 | 'info',
92 | 'Found existing task-master-ai MCP configuration in mcp.json, leaving untouched'
93 | );
94 | return; // Exit early, don't modify the existing configuration
95 | }
96 | // Add the task-master-ai server if it doesn't exist
97 | if (!mcpConfig.mcpServers['task-master-ai']) {
98 | mcpConfig.mcpServers['task-master-ai'] = newMCPServer['task-master-ai'];
99 | log(
100 | 'info',
101 | 'Added task-master-ai server to existing MCP configuration'
102 | );
103 | } else {
104 | log('info', 'task-master-ai server already configured in mcp.json');
105 | }
106 | // Write the updated configuration
107 | fs.writeFileSync(mcpPath, formatJSONWithTabs(mcpConfig) + '\n');
108 | log('success', 'Updated MCP configuration file');
109 | } catch (error) {
110 | log('error', `Failed to update MCP configuration: ${error.message}`);
111 | // Create a backup before potentially modifying
112 | const backupPath = `${mcpPath}.backup-${Date.now()}`;
113 | if (fs.existsSync(mcpPath)) {
114 | fs.copyFileSync(mcpPath, backupPath);
115 | log('info', `Created backup of existing mcp.json at ${backupPath}`);
116 | }
117 | // Create new configuration
118 | const newMCPConfig = {
119 | mcpServers: newMCPServer
120 | };
121 | fs.writeFileSync(mcpPath, formatJSONWithTabs(newMCPConfig) + '\n');
122 | log(
123 | 'warn',
124 | 'Created new MCP configuration file (backup of original file was created if it existed)'
125 | );
126 | }
127 | } else {
128 | // If mcp.json doesn't exist, create it
129 | const newMCPConfig = {
130 | mcpServers: newMCPServer
131 | };
132 | fs.writeFileSync(mcpPath, formatJSONWithTabs(newMCPConfig) + '\n');
133 | log('success', `Created MCP configuration file at ${mcpPath}`);
134 | }
135 |
136 | // Add note to console about MCP integration
137 | log('info', 'MCP server will use the installed task-master-ai package');
138 | }
139 |
140 | /**
141 | * Remove Task Master MCP server configuration from an existing mcp.json file
142 | * Only removes Task Master entries, preserving other MCP servers
143 | * @param {string} projectRoot - Target project directory
144 | * @param {string} mcpConfigPath - Relative path to MCP config file (e.g., '.cursor/mcp.json')
145 | * @returns {Object} Result object with success status and details
146 | */
147 | export function removeTaskMasterMCPConfiguration(projectRoot, mcpConfigPath) {
148 | // Handle null mcpConfigPath (e.g., for Claude/Codex profiles)
149 | if (!mcpConfigPath) {
150 | return {
151 | success: true,
152 | removed: false,
153 | deleted: false,
154 | error: null,
155 | hasOtherServers: false
156 | };
157 | }
158 |
159 | const mcpPath = path.join(projectRoot, mcpConfigPath);
160 |
161 | let result = {
162 | success: false,
163 | removed: false,
164 | deleted: false,
165 | error: null,
166 | hasOtherServers: false
167 | };
168 |
169 | if (!fs.existsSync(mcpPath)) {
170 | result.success = true;
171 | result.removed = false;
172 | log('debug', `[MCP Config] MCP config file does not exist: ${mcpPath}`);
173 | return result;
174 | }
175 |
176 | try {
177 | // Read existing config
178 | const mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8'));
179 |
180 | if (!mcpConfig.mcpServers) {
181 | result.success = true;
182 | result.removed = false;
183 | log('debug', `[MCP Config] No mcpServers section found in: ${mcpPath}`);
184 | return result;
185 | }
186 |
187 | // Check if Task Master is configured
188 | const hasTaskMaster =
189 | mcpConfig.mcpServers['task-master-ai'] ||
190 | Object.values(mcpConfig.mcpServers).some(
191 | (server) =>
192 | server.args &&
193 | Array.isArray(server.args) &&
194 | server.args.some(
195 | (arg) => typeof arg === 'string' && arg.includes('task-master-ai')
196 | )
197 | );
198 |
199 | if (!hasTaskMaster) {
200 | result.success = true;
201 | result.removed = false;
202 | log(
203 | 'debug',
204 | `[MCP Config] Task Master not found in MCP config: ${mcpPath}`
205 | );
206 | return result;
207 | }
208 |
209 | // Remove task-master-ai server
210 | delete mcpConfig.mcpServers['task-master-ai'];
211 |
212 | // Also remove any servers that have task-master-ai in their args
213 | Object.keys(mcpConfig.mcpServers).forEach((serverName) => {
214 | const server = mcpConfig.mcpServers[serverName];
215 | if (
216 | server.args &&
217 | Array.isArray(server.args) &&
218 | server.args.some(
219 | (arg) => typeof arg === 'string' && arg.includes('task-master-ai')
220 | )
221 | ) {
222 | delete mcpConfig.mcpServers[serverName];
223 | log(
224 | 'debug',
225 | `[MCP Config] Removed server '${serverName}' containing task-master-ai`
226 | );
227 | }
228 | });
229 |
230 | // Check if there are other MCP servers remaining
231 | const remainingServers = Object.keys(mcpConfig.mcpServers);
232 | result.hasOtherServers = remainingServers.length > 0;
233 |
234 | if (result.hasOtherServers) {
235 | // Write back the modified config with remaining servers
236 | fs.writeFileSync(mcpPath, formatJSONWithTabs(mcpConfig) + '\n');
237 | result.success = true;
238 | result.removed = true;
239 | result.deleted = false;
240 | log(
241 | 'info',
242 | `[MCP Config] Removed Task Master from MCP config, preserving other servers: ${remainingServers.join(', ')}`
243 | );
244 | } else {
245 | // No other servers, delete the entire file
246 | fs.rmSync(mcpPath, { force: true });
247 | result.success = true;
248 | result.removed = true;
249 | result.deleted = true;
250 | log(
251 | 'info',
252 | `[MCP Config] Removed MCP config file (no other servers remaining): ${mcpPath}`
253 | );
254 | }
255 | } catch (error) {
256 | result.error = error.message;
257 | log(
258 | 'error',
259 | `[MCP Config] Failed to remove Task Master from MCP config: ${error.message}`
260 | );
261 | }
262 |
263 | return result;
264 | }
265 |
266 | // Export the formatting function for use by other modules
267 | export { formatJSONWithTabs };
268 |
```
--------------------------------------------------------------------------------
/apps/extension/docs/extension-CI-setup.md:
--------------------------------------------------------------------------------
```markdown
1 | # VS Code Extension CI/CD Setup
2 |
3 | This document explains the CI/CD setup for the Task Master VS Code extension using automated changesets.
4 |
5 | ## 🔄 Workflows Overview
6 |
7 | ### 1. Extension CI (`extension-ci.yml`)
8 |
9 | #### Triggers
10 |
11 | - Push to `main` or `next` branches (only when extension files change)
12 | - Pull requests to `main` or `next` (only when extension files change)
13 |
14 | #### What it does
15 |
16 | - ✅ Lints and type-checks the extension code
17 | - 🔨 Builds the extension (`npm run build`)
18 | - 📦 Creates a clean package (`npm run package`)
19 | - 🧪 Runs tests with VS Code test framework
20 | - 📋 Creates a test VSIX package to verify packaging works
21 | - 💾 Uploads build artifacts for inspection
22 |
23 | ### 2. Version & Publish (`version.yml`)
24 |
25 | **Triggers:**
26 | - Push to `main` branch
27 |
28 | **What it does:**
29 | - 🔍 Detects changeset files for pending releases
30 | - 📝 Creates "Version Packages" PR with updated versions and CHANGELOG
31 | - 🤖 When Version PR is merged, automatically:
32 | - 🔨 Builds and packages the extension
33 | - 🏷️ Creates git tags with changeset automation
34 | - 📤 Publishes to VS Code Marketplace
35 | - 🌍 Publishes to Open VSX Registry
36 | - 📊 Updates package versions and CHANGELOG
37 |
38 | ## 🚀 Changeset Workflow
39 |
40 | ### Creating Changes
41 | When making changes to the extension:
42 |
43 | 1. **Make your code changes**
44 | 2. **Create a changeset**:
45 | ```bash
46 | # From project root
47 | npx changeset add
48 | ```
49 | 3. **Select the extension package**: Choose `taskr-kanban` when prompted
50 | 4. **Select version bump type**:
51 | - `patch`: Bug fixes, minor updates
52 | - `minor`: New features, backwards compatible
53 | - `major`: Breaking changes
54 | 5. **Write a summary**: Describe what changed for users
55 | 6. **Commit changeset file** along with your code changes
56 | 7. **Push to feature branch** and create PR
57 |
58 | ### Automated Publishing Process
59 | 1. **PR with changeset** gets merged to `main`
60 | 2. **Version workflow** detects changesets and creates "Version Packages" PR
61 | 3. **Review and merge** the Version PR
62 | 4. **Automated publishing** happens immediately:
63 | - Extension is built using 3-file packaging system
64 | - VSIX package is created and tested
65 | - Published to VS Code Marketplace (if `VSCE_PAT` is set)
66 | - Published to Open VSX Registry (if `OVSX_PAT` is set)
67 | - Git tags are created: `[email protected]`
68 | - CHANGELOG is updated automatically
69 |
70 | ## 🔑 Required Secrets
71 |
72 | To use the automated publishing, you need to set up these GitHub repository secrets:
73 |
74 | ### `VSCE_PAT` (VS Code Marketplace Personal Access Token)
75 | 1. Go to [Azure DevOps](https://dev.azure.com/)
76 | 2. Sign in with your Microsoft account
77 | 3. Create a Personal Access Token:
78 | - **Name**: VS Code Extension Publishing
79 | - **Organization**: All accessible organizations
80 | - **Expiration**: Custom (recommend 1 year)
81 | - **Scopes**: Custom defined → **Marketplace** → **Manage**
82 | 4. Copy the token and add it to GitHub Secrets as `VSCE_PAT`
83 |
84 | ### `OVSX_PAT` (Open VSX Registry Personal Access Token)
85 | 1. Go to [Open VSX Registry](https://open-vsx.org/)
86 | 2. Sign in with your GitHub account
87 | 3. Go to your [User Settings](https://open-vsx.org/user-settings/tokens)
88 | 4. Create a new Access Token:
89 | - **Description**: VS Code Extension Publishing
90 | - **Scopes**: Leave default (full access)
91 | 5. Copy the token and add it to GitHub Secrets as `OVSX_PAT`
92 |
93 | ### `GITHUB_TOKEN` (automatically provided)
94 | This is automatically available in GitHub Actions - no setup required.
95 |
96 | ## 📋 Version Management
97 |
98 | ### Changeset-Based Versioning
99 | Versions are automatically managed by changesets:
100 |
101 | - **No manual version updates needed** - changesets handle this automatically
102 | - **Semantic versioning** is enforced based on changeset types
103 | - **Changelog generation** happens automatically
104 | - **Git tagging** is handled by the automation
105 |
106 | ### Critical Fields Sync
107 | The automation ensures these fields stay in sync between `package.json` and `package.publish.json`:
108 |
109 | ```json
110 | {
111 | "version": "1.0.2", // ✅ AUTO-SYNCED
112 | "publisher": "Hamster", // ⚠️ MUST MATCH MANUALLY
113 | "displayName": "taskr: Task Master Kanban", // ⚠️ MUST MATCH MANUALLY
114 | "description": "...", // ⚠️ MUST MATCH MANUALLY
115 | "engines": { "vscode": "^1.93.0" }, // ⚠️ MUST MATCH MANUALLY
116 | "categories": [...], // ⚠️ MUST MATCH MANUALLY
117 | "contributes": { ... } // ⚠️ MUST MATCH MANUALLY
118 | }
119 | ```
120 |
121 | **Note**: Only `version` is automatically synced. Other fields must be manually kept in sync.
122 |
123 | ## 🔍 Monitoring Builds
124 |
125 | ### CI Status
126 |
127 | - **Green ✅**: Extension builds and tests successfully
128 | - **Red ❌**: Build/test failures - check logs for details
129 | - **Yellow 🟡**: Partial success - some jobs may have warnings
130 |
131 | ### Version PR Status
132 |
133 | - **Version PR Created**: Changesets detected, review and merge to publish
134 | - **No Version PR**: No changesets found, no releases pending
135 | - **Version PR Merged**: Automated publishing triggered
136 |
137 | ### Release Status
138 |
139 | - **Published 🎉**: Extension live on VS Code Marketplace and Open VSX
140 | - **Skipped ℹ️**: No changesets found, no release needed
141 | - **Failed ❌**: Check logs - often missing secrets or build issues
142 |
143 | ### Artifacts
144 |
145 | Workflows upload artifacts that you can download:
146 |
147 | - **CI**: Test results, built files, and VSIX package
148 | - **Version**: Final VSIX package and published extension
149 |
150 | ## 🛠️ Troubleshooting
151 |
152 | ### Common Issues
153 |
154 | #### No Version PR Created
155 |
156 | - **Check**: Changeset files exist in `.changeset/` directory
157 | - **Check**: Changeset refers to `taskr-kanban` package name
158 | - **Check**: Changes were pushed to `main` branch
159 | - **Solution**: Create changeset with `npx changeset add`
160 |
161 | #### Version PR Not Publishing
162 |
163 | - **Check**: Version PR was actually merged (not just closed)
164 | - **Check**: Required secrets (`VSCE_PAT`, `OVSX_PAT`) are set
165 | - **Check**: No build failures in workflow logs
166 | - **Solution**: Re-run workflow or check secret configuration
167 |
168 | #### `VSCE_PAT` is not set Error
169 |
170 | - Ensure `VSCE_PAT` secret is added to repository
171 | - Check token hasn't expired
172 | - Verify token has Marketplace → Manage permissions
173 |
174 | #### `OVSX_PAT` is not set Error
175 |
176 | - Ensure `OVSX_PAT` secret is added to repository
177 | - Check token hasn't expired
178 | - Verify you're signed in to Open VSX Registry with GitHub
179 |
180 | #### Build Failures
181 |
182 | - Check extension code compiles locally: `cd apps/extension && npm run build`
183 | - Verify tests pass locally: `npm run test`
184 | - Check for TypeScript errors: `npm run typecheck`
185 |
186 | #### Packaging Failures
187 |
188 | - Ensure clean package builds: `npm run package`
189 | - Check vsix-build structure is correct
190 | - Verify `package.publish.json` has correct `repository` field
191 |
192 | #### Changeset Issues
193 |
194 | - **Wrong package name**: Ensure changeset refers to `taskr-kanban`
195 | - **Invalid format**: Check changeset markdown format is correct
196 | - **Merge conflicts**: Resolve any conflicts in changeset files
197 |
198 | ## 📁 File Structure Impact
199 |
200 | The CI workflows respect the 3-file packaging system:
201 | - **Development**: Uses `package.json` for dependencies and scripts
202 | - **Release**: Uses `package.publish.json` for clean marketplace package
203 | - **Build**: Uses `package.mjs` to create `vsix-build/` for final packaging
204 | - **Changesets**: Automatically manage versions across the system
205 |
206 | ## 🌍 Dual Registry Publishing
207 |
208 | Your extension will be automatically published to both:
209 | - **VS Code Marketplace** - For official VS Code users
210 | - **Open VSX Registry** - For Cursor, Windsurf, VSCodium, Gitpod, Eclipse Theia, and other compatible editors
211 |
212 | ## 🎯 Benefits of Changeset Automation
213 |
214 | - ✅ **Automated versioning**: No manual version bumps needed
215 | - ✅ **Generated changelogs**: Automatic, accurate release notes
216 | - ✅ **Semantic versioning**: Enforced through changeset types
217 | - ✅ **Git tagging**: Proper tags for extension releases
218 | - ✅ **Conflict prevention**: Clear separation of extension vs. main package versions
219 | - ✅ **Review process**: Version changes are reviewable via PR
220 | - ✅ **Rollback capability**: Easy to revert if issues arise
221 |
222 | This ensures clean, predictable, and fully automated publishing to both registries! 🚀
223 |
```