This is page 60 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/utils.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * utils.js
3 | * Utility functions for the Task Master CLI
4 | */
5 |
6 | import fs from 'fs';
7 | import path from 'path';
8 | import chalk from 'chalk';
9 | import dotenv from 'dotenv';
10 | // Import specific config getters needed here
11 | import { getLogLevel, getDebugFlag } from './config-manager.js';
12 | import * as gitUtils from './utils/git-utils.js';
13 | import {
14 | COMPLEXITY_REPORT_FILE,
15 | LEGACY_COMPLEXITY_REPORT_FILE,
16 | LEGACY_CONFIG_FILE
17 | } from '../../src/constants/paths.js';
18 |
19 | // Global silent mode flag
20 | let silentMode = false;
21 |
22 | // --- Environment Variable Resolution Utility ---
23 | /**
24 | * Resolves an environment variable's value.
25 | * Precedence:
26 | * 1. session.env (if session provided)
27 | * 2. process.env
28 | * 3. .env file at projectRoot (if projectRoot provided)
29 | * @param {string} key - The environment variable key.
30 | * @param {object|null} [session=null] - The MCP session object.
31 | * @param {string|null} [projectRoot=null] - The project root directory (for .env fallback).
32 | * @returns {string|undefined} The value of the environment variable or undefined if not found.
33 | */
34 | function resolveEnvVariable(key, session = null, projectRoot = null) {
35 | // 1. Check session.env
36 | if (session?.env?.[key]) {
37 | return session.env[key];
38 | }
39 |
40 | // 2. Read .env file at projectRoot
41 | if (projectRoot) {
42 | const envPath = path.join(projectRoot, '.env');
43 | if (fs.existsSync(envPath)) {
44 | try {
45 | const envFileContent = fs.readFileSync(envPath, 'utf-8');
46 | const parsedEnv = dotenv.parse(envFileContent); // Use dotenv to parse
47 | if (parsedEnv && parsedEnv[key]) {
48 | // console.log(`DEBUG: Found key ${key} in ${envPath}`); // Optional debug log
49 | return parsedEnv[key];
50 | }
51 | } catch (error) {
52 | // Log error but don't crash, just proceed as if key wasn't found in file
53 | log('warn', `Could not read or parse ${envPath}: ${error.message}`);
54 | }
55 | }
56 | }
57 |
58 | // 3. Fallback: Check process.env
59 | if (process.env[key]) {
60 | return process.env[key];
61 | }
62 |
63 | // Not found anywhere
64 | return undefined;
65 | }
66 |
67 | // --- Tag-Aware Path Resolution Utility ---
68 |
69 | /**
70 | * Slugifies a tag name to be filesystem-safe
71 | * @param {string} tagName - The tag name to slugify
72 | * @returns {string} Slugified tag name safe for filesystem use
73 | */
74 | function slugifyTagForFilePath(tagName) {
75 | if (!tagName || typeof tagName !== 'string') {
76 | return 'unknown-tag';
77 | }
78 |
79 | // Replace invalid filesystem characters with hyphens and clean up
80 | return tagName
81 | .replace(/[^a-zA-Z0-9_-]/g, '-') // Replace invalid chars with hyphens
82 | .replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
83 | .replace(/-+/g, '-') // Collapse multiple hyphens
84 | .toLowerCase() // Convert to lowercase
85 | .substring(0, 50); // Limit length to prevent overly long filenames
86 | }
87 |
88 | /**
89 | * Resolves a file path to be tag-aware, following the pattern used by other commands.
90 | * For non-master tags, appends _slugified-tagname before the file extension.
91 | * @param {string} basePath - The base file path (e.g., '.taskmaster/reports/task-complexity-report.json')
92 | * @param {string|null} tag - The tag name (null, undefined, or 'master' uses base path)
93 | * @param {string} [projectRoot='.'] - The project root directory
94 | * @returns {string} The resolved file path
95 | */
96 | function getTagAwareFilePath(basePath, tag, projectRoot = '.') {
97 | // Use path.parse and format for clean tag insertion
98 | const parsedPath = path.parse(basePath);
99 | if (!tag || tag === 'master') {
100 | return path.join(projectRoot, basePath);
101 | }
102 |
103 | // Slugify the tag for filesystem safety
104 | const slugifiedTag = slugifyTagForFilePath(tag);
105 |
106 | // Append slugified tag before file extension
107 | parsedPath.base = `${parsedPath.name}_${slugifiedTag}${parsedPath.ext}`;
108 | const relativePath = path.format(parsedPath);
109 | return path.join(projectRoot, relativePath);
110 | }
111 |
112 | // --- Project Root Finding Utility ---
113 | /**
114 | * Recursively searches upwards for project root starting from a given directory.
115 | * @param {string} [startDir=process.cwd()] - The directory to start searching from.
116 | * @param {string[]} [markers=['package.json', '.git', LEGACY_CONFIG_FILE]] - Marker files/dirs to look for.
117 | * @returns {string|null} The path to the project root, or null if not found.
118 | */
119 | function findProjectRoot(
120 | startDir = process.cwd(),
121 | markers = ['package.json', 'pyproject.toml', '.git', LEGACY_CONFIG_FILE]
122 | ) {
123 | let currentPath = path.resolve(startDir);
124 | const rootPath = path.parse(currentPath).root;
125 |
126 | while (currentPath !== rootPath) {
127 | // Check if any marker exists in the current directory
128 | const hasMarker = markers.some((marker) => {
129 | const markerPath = path.join(currentPath, marker);
130 | return fs.existsSync(markerPath);
131 | });
132 |
133 | if (hasMarker) {
134 | return currentPath;
135 | }
136 |
137 | // Move up one directory
138 | currentPath = path.dirname(currentPath);
139 | }
140 |
141 | // Check the root directory as well
142 | const hasMarkerInRoot = markers.some((marker) => {
143 | const markerPath = path.join(rootPath, marker);
144 | return fs.existsSync(markerPath);
145 | });
146 |
147 | return hasMarkerInRoot ? rootPath : null;
148 | }
149 |
150 | // --- Dynamic Configuration Function --- (REMOVED)
151 |
152 | // --- Logging and Utility Functions ---
153 |
154 | // Set up logging based on log level
155 | const LOG_LEVELS = {
156 | debug: 0,
157 | info: 1,
158 | warn: 2,
159 | error: 3,
160 | success: 1 // Treat success like info level
161 | };
162 |
163 | /**
164 | * Returns the task manager module
165 | * @returns {Promise<Object>} The task manager module object
166 | */
167 | async function getTaskManager() {
168 | return import('./task-manager.js');
169 | }
170 |
171 | /**
172 | * Enable silent logging mode
173 | */
174 | function enableSilentMode() {
175 | silentMode = true;
176 | }
177 |
178 | /**
179 | * Disable silent logging mode
180 | */
181 | function disableSilentMode() {
182 | silentMode = false;
183 | }
184 |
185 | /**
186 | * Check if silent mode is enabled
187 | * @returns {boolean} True if silent mode is enabled
188 | */
189 | function isSilentMode() {
190 | return silentMode;
191 | }
192 |
193 | /**
194 | * Logs a message at the specified level
195 | * @param {string} level - The log level (debug, info, warn, error)
196 | * @param {...any} args - Arguments to log
197 | */
198 | function log(level, ...args) {
199 | // Immediately return if silentMode is enabled
200 | if (isSilentMode()) {
201 | return;
202 | }
203 |
204 | // GUARD: Prevent circular dependency during config loading
205 | // Use a simple fallback log level instead of calling getLogLevel()
206 | let configLevel = 'info'; // Default fallback
207 | try {
208 | // Only try to get config level if we're not in the middle of config loading
209 | configLevel = getLogLevel() || 'info';
210 | } catch (error) {
211 | // If getLogLevel() fails (likely due to circular dependency),
212 | // use default 'info' level and continue
213 | configLevel = 'info';
214 | }
215 |
216 | // Use text prefixes instead of emojis
217 | const prefixes = {
218 | debug: chalk.gray('[DEBUG]'),
219 | info: chalk.blue('[INFO]'),
220 | warn: chalk.yellow('[WARN]'),
221 | error: chalk.red('[ERROR]'),
222 | success: chalk.green('[SUCCESS]')
223 | };
224 |
225 | // Ensure level exists, default to info if not
226 | const currentLevel = LOG_LEVELS.hasOwnProperty(level) ? level : 'info';
227 |
228 | // Check log level configuration
229 | if (
230 | LOG_LEVELS[currentLevel] >= (LOG_LEVELS[configLevel] ?? LOG_LEVELS.info)
231 | ) {
232 | const prefix = prefixes[currentLevel] || '';
233 | // Use console.log for all levels, let chalk handle coloring
234 | // Construct the message properly
235 | const message = args
236 | .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg))
237 | .join(' ');
238 | console.log(`${prefix} ${message}`);
239 | }
240 | }
241 |
242 | /**
243 | * Checks if the data object has a tagged structure (contains tag objects with tasks arrays)
244 | * @param {Object} data - The data object to check
245 | * @returns {boolean} True if the data has a tagged structure
246 | */
247 | function hasTaggedStructure(data) {
248 | if (!data || typeof data !== 'object') {
249 | return false;
250 | }
251 |
252 | // Check if any top-level properties are objects with tasks arrays
253 | for (const key in data) {
254 | if (
255 | data.hasOwnProperty(key) &&
256 | typeof data[key] === 'object' &&
257 | Array.isArray(data[key].tasks)
258 | ) {
259 | return true;
260 | }
261 | }
262 | return false;
263 | }
264 |
265 | /**
266 | * Normalizes task IDs to ensure they are numbers instead of strings
267 | * @param {Array} tasks - Array of tasks to normalize
268 | */
269 | function normalizeTaskIds(tasks) {
270 | if (!Array.isArray(tasks)) return;
271 |
272 | tasks.forEach((task) => {
273 | // Convert task ID to number with validation
274 | if (task.id !== undefined) {
275 | const parsedId = parseInt(task.id, 10);
276 | if (!isNaN(parsedId) && parsedId > 0) {
277 | task.id = parsedId;
278 | }
279 | }
280 |
281 | // Convert subtask IDs to numbers with validation
282 | if (Array.isArray(task.subtasks)) {
283 | task.subtasks.forEach((subtask) => {
284 | if (subtask.id !== undefined) {
285 | // Check for dot notation (which shouldn't exist in storage)
286 | if (typeof subtask.id === 'string' && subtask.id.includes('.')) {
287 | // Extract the subtask part after the dot
288 | const parts = subtask.id.split('.');
289 | subtask.id = parseInt(parts[parts.length - 1], 10);
290 | } else {
291 | const parsedSubtaskId = parseInt(subtask.id, 10);
292 | if (!isNaN(parsedSubtaskId) && parsedSubtaskId > 0) {
293 | subtask.id = parsedSubtaskId;
294 | }
295 | }
296 | }
297 | });
298 | }
299 | });
300 | }
301 |
302 | /**
303 | * Reads and parses a JSON file
304 | * @param {string} filepath - Path to the JSON file
305 | * @param {string} [projectRoot] - Optional project root for tag resolution (used by MCP)
306 | * @param {string} [tag] - Optional tag to use instead of current tag resolution
307 | * @returns {Object|null} The parsed JSON data or null if error
308 | */
309 | function readJSON(filepath, projectRoot = null, tag = null) {
310 | // GUARD: Prevent circular dependency during config loading
311 | let isDebug = false; // Default fallback
312 | try {
313 | // Only try to get debug flag if we're not in the middle of config loading
314 | isDebug = getDebugFlag();
315 | } catch (error) {
316 | // If getDebugFlag() fails (likely due to circular dependency),
317 | // use default false and continue
318 | }
319 |
320 | if (isDebug) {
321 | console.log(
322 | `readJSON called with: ${filepath}, projectRoot: ${projectRoot}, tag: ${tag}`
323 | );
324 | }
325 |
326 | if (!filepath) {
327 | return null;
328 | }
329 |
330 | let data;
331 | try {
332 | data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
333 | if (isDebug) {
334 | console.log(`Successfully read JSON from ${filepath}`);
335 | }
336 | } catch (err) {
337 | if (isDebug) {
338 | console.log(`Failed to read JSON from ${filepath}: ${err.message}`);
339 | }
340 | return null;
341 | }
342 |
343 | // If it's not a tasks.json file, return as-is
344 | if (!filepath.includes('tasks.json') || !data) {
345 | if (isDebug) {
346 | console.log(`File is not tasks.json or data is null, returning as-is`);
347 | }
348 | return data;
349 | }
350 |
351 | // Check if this is legacy format that needs migration
352 | // Only migrate if we have tasks at the ROOT level AND no tag-like structure
353 | if (
354 | Array.isArray(data.tasks) &&
355 | !data._rawTaggedData &&
356 | !hasTaggedStructure(data)
357 | ) {
358 | if (isDebug) {
359 | console.log(`File is in legacy format, performing migration...`);
360 | }
361 |
362 | normalizeTaskIds(data.tasks);
363 |
364 | // This is legacy format - migrate it to tagged format
365 | const migratedData = {
366 | master: {
367 | tasks: data.tasks,
368 | metadata: data.metadata || {
369 | created: new Date().toISOString(),
370 | updated: new Date().toISOString(),
371 | description: 'Tasks for master context'
372 | }
373 | }
374 | };
375 |
376 | // Write the migrated data back to the file
377 | try {
378 | writeJSON(filepath, migratedData);
379 | if (isDebug) {
380 | console.log(`Successfully migrated legacy format to tagged format`);
381 | }
382 |
383 | // Perform complete migration (config.json, state.json)
384 | performCompleteTagMigration(filepath);
385 |
386 | // Check and auto-switch git tags if enabled (after migration)
387 | // This needs to run synchronously BEFORE tag resolution
388 | if (projectRoot) {
389 | try {
390 | // Run git integration synchronously
391 | gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath);
392 | } catch (error) {
393 | // Silent fail - don't break normal operations
394 | }
395 | }
396 |
397 | // Mark for migration notice
398 | markMigrationForNotice(filepath);
399 | } catch (writeError) {
400 | if (isDebug) {
401 | console.log(`Error writing migrated data: ${writeError.message}`);
402 | }
403 | // If write fails, continue with the original data
404 | }
405 |
406 | // Continue processing with the migrated data structure
407 | data = migratedData;
408 | }
409 |
410 | // If we have tagged data, we need to resolve which tag to use
411 | if (typeof data === 'object' && !data.tasks) {
412 | // This is tagged format
413 | if (isDebug) {
414 | console.log(`File is in tagged format, resolving tag...`);
415 | }
416 |
417 | // Ensure all tags have proper metadata before proceeding
418 | for (const tagName in data) {
419 | if (
420 | data.hasOwnProperty(tagName) &&
421 | typeof data[tagName] === 'object' &&
422 | data[tagName].tasks
423 | ) {
424 | try {
425 | ensureTagMetadata(data[tagName], {
426 | description: `Tasks for ${tagName} context`,
427 | skipUpdate: true // Don't update timestamp during read operations
428 | });
429 | } catch (error) {
430 | // If ensureTagMetadata fails, continue without metadata
431 | if (isDebug) {
432 | console.log(
433 | `Failed to ensure metadata for tag ${tagName}: ${error.message}`
434 | );
435 | }
436 | }
437 | }
438 | }
439 |
440 | // Store reference to the raw tagged data for functions that need it
441 | const originalTaggedData = JSON.parse(JSON.stringify(data));
442 |
443 | // Normalize IDs in all tags before storing as originalTaggedData
444 | for (const tagName in originalTaggedData) {
445 | if (
446 | originalTaggedData[tagName] &&
447 | Array.isArray(originalTaggedData[tagName].tasks)
448 | ) {
449 | normalizeTaskIds(originalTaggedData[tagName].tasks);
450 | }
451 | }
452 |
453 | // Check and auto-switch git tags if enabled (for existing tagged format)
454 | // This needs to run synchronously BEFORE tag resolution
455 | if (projectRoot) {
456 | try {
457 | // Run git integration synchronously
458 | gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath);
459 | } catch (error) {
460 | // Silent fail - don't break normal operations
461 | }
462 | }
463 |
464 | try {
465 | // Default to master tag if anything goes wrong
466 | let resolvedTag = 'master';
467 |
468 | // Try to resolve the correct tag, but don't fail if it doesn't work
469 | try {
470 | // If tag is provided, use it directly
471 | if (tag) {
472 | resolvedTag = tag;
473 | } else if (projectRoot) {
474 | // Use provided projectRoot
475 | resolvedTag = resolveTag({ projectRoot });
476 | } else {
477 | // Try to derive projectRoot from filepath
478 | const derivedProjectRoot = findProjectRoot(path.dirname(filepath));
479 | if (derivedProjectRoot) {
480 | resolvedTag = resolveTag({ projectRoot: derivedProjectRoot });
481 | }
482 | // If derivedProjectRoot is null, stick with 'master'
483 | }
484 | } catch (tagResolveError) {
485 | if (isDebug) {
486 | console.log(
487 | `Tag resolution failed, using master: ${tagResolveError.message}`
488 | );
489 | }
490 | // resolvedTag stays as 'master'
491 | }
492 |
493 | if (isDebug) {
494 | console.log(`Resolved tag: ${resolvedTag}`);
495 | }
496 |
497 | // Get the data for the resolved tag
498 | const tagData = data[resolvedTag];
499 | if (tagData && tagData.tasks) {
500 | normalizeTaskIds(tagData.tasks);
501 |
502 | // Add the _rawTaggedData property and the resolved tag to the returned data
503 | const result = {
504 | ...tagData,
505 | tag: resolvedTag,
506 | _rawTaggedData: originalTaggedData
507 | };
508 | if (isDebug) {
509 | console.log(
510 | `Returning data for tag '${resolvedTag}' with ${tagData.tasks.length} tasks`
511 | );
512 | }
513 | return result;
514 | } else {
515 | // If the resolved tag doesn't exist, fall back to master
516 | const masterData = data.master;
517 | if (masterData && masterData.tasks) {
518 | normalizeTaskIds(masterData.tasks);
519 |
520 | if (isDebug) {
521 | console.log(
522 | `Tag '${resolvedTag}' not found, falling back to master with ${masterData.tasks.length} tasks`
523 | );
524 | }
525 | return {
526 | ...masterData,
527 | tag: 'master',
528 | _rawTaggedData: originalTaggedData
529 | };
530 | } else {
531 | if (isDebug) {
532 | console.log(`No valid tag data found, returning empty structure`);
533 | }
534 | // Return empty structure if no valid data
535 | return {
536 | tasks: [],
537 | tag: 'master',
538 | _rawTaggedData: originalTaggedData
539 | };
540 | }
541 | }
542 | } catch (error) {
543 | if (isDebug) {
544 | console.log(`Error during tag resolution: ${error.message}`);
545 | }
546 | // If anything goes wrong, try to return master or empty
547 | const masterData = data.master;
548 | if (masterData && masterData.tasks) {
549 | normalizeTaskIds(masterData.tasks);
550 | return {
551 | ...masterData,
552 | _rawTaggedData: originalTaggedData
553 | };
554 | }
555 | return {
556 | tasks: [],
557 | _rawTaggedData: originalTaggedData
558 | };
559 | }
560 | }
561 |
562 | // If we reach here, it's some other format
563 | if (isDebug) {
564 | console.log(`File format not recognized, returning as-is`);
565 | }
566 | return data;
567 | }
568 |
569 | /**
570 | * Performs complete tag migration including config.json and state.json updates
571 | * @param {string} tasksJsonPath - Path to the tasks.json file that was migrated
572 | */
573 | function performCompleteTagMigration(tasksJsonPath) {
574 | try {
575 | // Derive project root from tasks.json path
576 | const projectRoot =
577 | findProjectRoot(path.dirname(tasksJsonPath)) ||
578 | path.dirname(tasksJsonPath);
579 |
580 | // 1. Migrate config.json - add defaultTag and tags section
581 | const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
582 | if (fs.existsSync(configPath)) {
583 | migrateConfigJson(configPath);
584 | }
585 |
586 | // 2. Create state.json if it doesn't exist
587 | const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
588 | if (!fs.existsSync(statePath)) {
589 | createStateJson(statePath);
590 | }
591 |
592 | if (getDebugFlag()) {
593 | log(
594 | 'debug',
595 | `Complete tag migration performed for project: ${projectRoot}`
596 | );
597 | }
598 | } catch (error) {
599 | if (getDebugFlag()) {
600 | log('warn', `Error during complete tag migration: ${error.message}`);
601 | }
602 | }
603 | }
604 |
605 | /**
606 | * Migrates config.json to add tagged task system configuration
607 | * @param {string} configPath - Path to the config.json file
608 | */
609 | function migrateConfigJson(configPath) {
610 | try {
611 | const rawConfig = fs.readFileSync(configPath, 'utf8');
612 | const config = JSON.parse(rawConfig);
613 | if (!config) return;
614 |
615 | let modified = false;
616 |
617 | // Add global.defaultTag if missing
618 | if (!config.global) {
619 | config.global = {};
620 | }
621 | if (!config.global.defaultTag) {
622 | config.global.defaultTag = 'master';
623 | modified = true;
624 | }
625 |
626 | if (modified) {
627 | fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
628 | if (process.env.TASKMASTER_DEBUG === 'true') {
629 | console.log(
630 | '[DEBUG] Updated config.json with tagged task system settings'
631 | );
632 | }
633 | }
634 | } catch (error) {
635 | if (process.env.TASKMASTER_DEBUG === 'true') {
636 | console.warn(`[WARN] Error migrating config.json: ${error.message}`);
637 | }
638 | }
639 | }
640 |
641 | /**
642 | * Creates initial state.json file for tagged task system
643 | * @param {string} statePath - Path where state.json should be created
644 | */
645 | function createStateJson(statePath) {
646 | try {
647 | const initialState = {
648 | currentTag: 'master',
649 | lastSwitched: new Date().toISOString(),
650 | branchTagMapping: {},
651 | migrationNoticeShown: false
652 | };
653 |
654 | fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2), 'utf8');
655 | if (process.env.TASKMASTER_DEBUG === 'true') {
656 | console.log('[DEBUG] Created initial state.json for tagged task system');
657 | }
658 | } catch (error) {
659 | if (process.env.TASKMASTER_DEBUG === 'true') {
660 | console.warn(`[WARN] Error creating state.json: ${error.message}`);
661 | }
662 | }
663 | }
664 |
665 | /**
666 | * Marks in state.json that migration occurred and notice should be shown
667 | * @param {string} tasksJsonPath - Path to the tasks.json file
668 | */
669 | function markMigrationForNotice(tasksJsonPath) {
670 | try {
671 | const projectRoot = path.dirname(path.dirname(tasksJsonPath));
672 | const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
673 |
674 | // Ensure state.json exists
675 | if (!fs.existsSync(statePath)) {
676 | createStateJson(statePath);
677 | }
678 |
679 | // Read and update state to mark migration occurred using fs directly
680 | try {
681 | const rawState = fs.readFileSync(statePath, 'utf8');
682 | const stateData = JSON.parse(rawState) || {};
683 | // Only set to false if it's not already set (i.e., first time migration)
684 | if (stateData.migrationNoticeShown === undefined) {
685 | stateData.migrationNoticeShown = false;
686 | fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2), 'utf8');
687 | }
688 | } catch (stateError) {
689 | if (process.env.TASKMASTER_DEBUG === 'true') {
690 | console.warn(
691 | `[WARN] Error updating state for migration notice: ${stateError.message}`
692 | );
693 | }
694 | }
695 | } catch (error) {
696 | if (process.env.TASKMASTER_DEBUG === 'true') {
697 | console.warn(
698 | `[WARN] Error marking migration for notice: ${error.message}`
699 | );
700 | }
701 | }
702 | }
703 |
704 | /**
705 | * Writes and saves a JSON file. Handles tagged task lists properly.
706 | * @param {string} filepath - Path to the JSON file
707 | * @param {Object} data - Data to write (can be resolved tag data or raw tagged data)
708 | * @param {string} projectRoot - Optional project root for tag context
709 | * @param {string} tag - Optional tag for tag context
710 | */
711 | function writeJSON(filepath, data, projectRoot = null, tag = null) {
712 | const isDebug = process.env.TASKMASTER_DEBUG === 'true';
713 |
714 | try {
715 | let finalData = data;
716 |
717 | // If data represents resolved tag data but lost _rawTaggedData (edge-case observed in MCP path)
718 | if (
719 | !data._rawTaggedData &&
720 | projectRoot &&
721 | Array.isArray(data.tasks) &&
722 | !hasTaggedStructure(data)
723 | ) {
724 | const resolvedTag = tag || getCurrentTag(projectRoot);
725 |
726 | if (isDebug) {
727 | console.log(
728 | `writeJSON: Detected resolved tag data missing _rawTaggedData. Re-reading raw data to prevent data loss for tag '${resolvedTag}'.`
729 | );
730 | }
731 |
732 | // Re-read the full file to get the complete tagged structure
733 | const rawFullData = JSON.parse(fs.readFileSync(filepath, 'utf8'));
734 |
735 | // Merge the updated data into the full structure
736 | finalData = {
737 | ...rawFullData,
738 | [resolvedTag]: {
739 | // Preserve existing tag metadata if it exists, otherwise use what's passed
740 | ...(rawFullData[resolvedTag]?.metadata || {}),
741 | ...(data.metadata ? { metadata: data.metadata } : {}),
742 | tasks: data.tasks // The updated tasks array is the source of truth here
743 | }
744 | };
745 | }
746 | // If we have _rawTaggedData, this means we're working with resolved tag data
747 | // and need to merge it back into the full tagged structure
748 | else if (data && data._rawTaggedData && projectRoot) {
749 | const resolvedTag = tag || getCurrentTag(projectRoot);
750 |
751 | // Get the original tagged data
752 | const originalTaggedData = data._rawTaggedData;
753 |
754 | // Create a clean copy of the current resolved data (without internal properties)
755 | const { _rawTaggedData, tag: _, ...cleanResolvedData } = data;
756 |
757 | // Update the specific tag with the resolved data
758 | finalData = {
759 | ...originalTaggedData,
760 | [resolvedTag]: cleanResolvedData
761 | };
762 |
763 | if (isDebug) {
764 | console.log(
765 | `writeJSON: Merging resolved data back into tag '${resolvedTag}'`
766 | );
767 | }
768 | }
769 |
770 | // Clean up any internal properties that shouldn't be persisted
771 | let cleanData = finalData;
772 | if (cleanData && typeof cleanData === 'object') {
773 | // Remove any _rawTaggedData or tag properties from root level
774 | const { _rawTaggedData, tag: tagProp, ...rootCleanData } = cleanData;
775 | cleanData = rootCleanData;
776 |
777 | // Additional cleanup for tag objects
778 | if (typeof cleanData === 'object' && !Array.isArray(cleanData)) {
779 | const finalCleanData = {};
780 | for (const [key, value] of Object.entries(cleanData)) {
781 | if (
782 | value &&
783 | typeof value === 'object' &&
784 | Array.isArray(value.tasks)
785 | ) {
786 | // This is a tag object - clean up any rogue root-level properties
787 | const { created, description, ...cleanTagData } = value;
788 |
789 | // Only keep the description if there's no metadata.description
790 | if (
791 | description &&
792 | (!cleanTagData.metadata || !cleanTagData.metadata.description)
793 | ) {
794 | cleanTagData.description = description;
795 | }
796 |
797 | finalCleanData[key] = cleanTagData;
798 | } else {
799 | finalCleanData[key] = value;
800 | }
801 | }
802 | cleanData = finalCleanData;
803 | }
804 | }
805 |
806 | fs.writeFileSync(filepath, JSON.stringify(cleanData, null, 2), 'utf8');
807 |
808 | if (isDebug) {
809 | console.log(`writeJSON: Successfully wrote to ${filepath}`);
810 | }
811 | } catch (error) {
812 | log('error', `Error writing JSON file ${filepath}:`, error.message);
813 | if (isDebug) {
814 | log('error', 'Full error details:', error);
815 | }
816 | }
817 | }
818 |
819 | /**
820 | * Sanitizes a prompt string for use in a shell command
821 | * @param {string} prompt The prompt to sanitize
822 | * @returns {string} Sanitized prompt
823 | */
824 | function sanitizePrompt(prompt) {
825 | // Replace double quotes with escaped double quotes
826 | return prompt.replace(/"/g, '\\"');
827 | }
828 |
829 | /**
830 | * Reads the complexity report from file
831 | * @param {string} customPath - Optional custom path to the report
832 | * @returns {Object|null} The parsed complexity report or null if not found
833 | */
834 | function readComplexityReport(customPath = null) {
835 | // GUARD: Prevent circular dependency during config loading
836 | let isDebug = false; // Default fallback
837 | try {
838 | // Only try to get debug flag if we're not in the middle of config loading
839 | isDebug = getDebugFlag();
840 | } catch (error) {
841 | // If getDebugFlag() fails (likely due to circular dependency),
842 | // use default false and continue
843 | isDebug = false;
844 | }
845 |
846 | try {
847 | let reportPath;
848 | if (customPath) {
849 | reportPath = customPath;
850 | } else {
851 | // Try new location first, then fall back to legacy
852 | const newPath = path.join(process.cwd(), COMPLEXITY_REPORT_FILE);
853 | const legacyPath = path.join(
854 | process.cwd(),
855 | LEGACY_COMPLEXITY_REPORT_FILE
856 | );
857 |
858 | reportPath = fs.existsSync(newPath) ? newPath : legacyPath;
859 | }
860 |
861 | if (!fs.existsSync(reportPath)) {
862 | if (isDebug) {
863 | log('debug', `Complexity report not found at ${reportPath}`);
864 | }
865 | return null;
866 | }
867 |
868 | const reportData = readJSON(reportPath);
869 | if (isDebug) {
870 | log('debug', `Successfully read complexity report from ${reportPath}`);
871 | }
872 | return reportData;
873 | } catch (error) {
874 | if (isDebug) {
875 | log('error', `Error reading complexity report: ${error.message}`);
876 | }
877 | return null;
878 | }
879 | }
880 |
881 | /**
882 | * Finds a task analysis in the complexity report
883 | * @param {Object} report - The complexity report
884 | * @param {number} taskId - The task ID to find
885 | * @returns {Object|null} The task analysis or null if not found
886 | */
887 | function findTaskInComplexityReport(report, taskId) {
888 | if (
889 | !report ||
890 | !report.complexityAnalysis ||
891 | !Array.isArray(report.complexityAnalysis)
892 | ) {
893 | return null;
894 | }
895 |
896 | return report.complexityAnalysis.find((task) => task.taskId === taskId);
897 | }
898 |
899 | function addComplexityToTask(task, complexityReport) {
900 | let taskId;
901 | if (task.isSubtask) {
902 | taskId = task.parentTask.id;
903 | } else if (task.parentId) {
904 | taskId = task.parentId;
905 | } else {
906 | taskId = task.id;
907 | }
908 |
909 | const taskAnalysis = findTaskInComplexityReport(complexityReport, taskId);
910 | if (taskAnalysis) {
911 | task.complexityScore = taskAnalysis.complexityScore;
912 | }
913 | }
914 |
915 | /**
916 | * Checks if a task exists in the tasks array
917 | * @param {Array} tasks - The tasks array
918 | * @param {string|number} taskId - The task ID to check
919 | * @returns {boolean} True if the task exists, false otherwise
920 | */
921 | function taskExists(tasks, taskId) {
922 | if (!taskId || !tasks || !Array.isArray(tasks)) {
923 | return false;
924 | }
925 |
926 | // Handle both regular task IDs and subtask IDs (e.g., "1.2")
927 | if (typeof taskId === 'string' && taskId.includes('.')) {
928 | const [parentId, subtaskId] = taskId
929 | .split('.')
930 | .map((id) => parseInt(id, 10));
931 | const parentTask = tasks.find((t) => t.id === parentId);
932 |
933 | if (!parentTask || !parentTask.subtasks) {
934 | return false;
935 | }
936 |
937 | return parentTask.subtasks.some((st) => st.id === subtaskId);
938 | }
939 |
940 | const id = parseInt(taskId, 10);
941 | return tasks.some((t) => t.id === id);
942 | }
943 |
944 | /**
945 | * Formats a task ID as a string
946 | * @param {string|number} id - The task ID to format
947 | * @returns {string} The formatted task ID
948 | */
949 | function formatTaskId(id) {
950 | if (typeof id === 'string' && id.includes('.')) {
951 | return id; // Already formatted as a string with a dot (e.g., "1.2")
952 | }
953 |
954 | if (typeof id === 'number') {
955 | return id.toString();
956 | }
957 |
958 | return id;
959 | }
960 |
961 | /**
962 | * Finds a task by ID in the tasks array. Optionally filters subtasks by status.
963 | * @param {Array} tasks - The tasks array
964 | * @param {string|number} taskId - The task ID to find
965 | * @param {Object|null} complexityReport - Optional pre-loaded complexity report
966 | * @param {string} [statusFilter] - Optional status to filter subtasks by
967 | * @returns {{task: Object|null, originalSubtaskCount: number|null, originalSubtasks: Array|null}} The task object (potentially with filtered subtasks), the original subtask count, and original subtasks array if filtered, or nulls if not found.
968 | */
969 | function findTaskById(
970 | tasks,
971 | taskId,
972 | complexityReport = null,
973 | statusFilter = null
974 | ) {
975 | if (!taskId || !tasks || !Array.isArray(tasks)) {
976 | return { task: null, originalSubtaskCount: null };
977 | }
978 |
979 | // Check if it's a subtask ID (e.g., "1.2")
980 | if (typeof taskId === 'string' && taskId.includes('.')) {
981 | // If looking for a subtask, statusFilter doesn't apply directly here.
982 | const [parentId, subtaskId] = taskId
983 | .split('.')
984 | .map((id) => parseInt(id, 10));
985 | const parentTask = tasks.find((t) => t.id === parentId);
986 |
987 | if (!parentTask || !parentTask.subtasks) {
988 | return { task: null, originalSubtaskCount: null, originalSubtasks: null };
989 | }
990 |
991 | const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
992 | if (subtask) {
993 | // Add reference to parent task for context
994 | subtask.parentTask = {
995 | id: parentTask.id,
996 | title: parentTask.title,
997 | status: parentTask.status
998 | };
999 | subtask.isSubtask = true;
1000 | }
1001 |
1002 | // If we found a task, check for complexity data
1003 | if (subtask && complexityReport) {
1004 | addComplexityToTask(subtask, complexityReport);
1005 | }
1006 |
1007 | return {
1008 | task: subtask || null,
1009 | originalSubtaskCount: null,
1010 | originalSubtasks: null
1011 | };
1012 | }
1013 |
1014 | let taskResult = null;
1015 | let originalSubtaskCount = null;
1016 | let originalSubtasks = null;
1017 |
1018 | // Find the main task
1019 | const id = parseInt(taskId, 10);
1020 | const task = tasks.find((t) => t.id === id) || null;
1021 |
1022 | // If task not found, return nulls
1023 | if (!task) {
1024 | return { task: null, originalSubtaskCount: null, originalSubtasks: null };
1025 | }
1026 |
1027 | taskResult = task;
1028 |
1029 | // If task found and statusFilter provided, filter its subtasks
1030 | if (statusFilter && task.subtasks && Array.isArray(task.subtasks)) {
1031 | // Store original subtasks and count before filtering
1032 | originalSubtasks = [...task.subtasks]; // Clone the original subtasks array
1033 | originalSubtaskCount = task.subtasks.length;
1034 |
1035 | // Clone the task to avoid modifying the original array
1036 | const filteredTask = { ...task };
1037 | filteredTask.subtasks = task.subtasks.filter(
1038 | (subtask) =>
1039 | subtask.status &&
1040 | subtask.status.toLowerCase() === statusFilter.toLowerCase()
1041 | );
1042 |
1043 | taskResult = filteredTask;
1044 | }
1045 |
1046 | // If task found and complexityReport provided, add complexity data
1047 | if (taskResult && complexityReport) {
1048 | addComplexityToTask(taskResult, complexityReport);
1049 | }
1050 |
1051 | // Return the found task, original subtask count, and original subtasks
1052 | return { task: taskResult, originalSubtaskCount, originalSubtasks };
1053 | }
1054 |
1055 | /**
1056 | * Truncates text to a specified length
1057 | * @param {string} text - The text to truncate
1058 | * @param {number} maxLength - The maximum length
1059 | * @returns {string} The truncated text
1060 | */
1061 | function truncate(text, maxLength) {
1062 | if (!text || text.length <= maxLength) {
1063 | return text;
1064 | }
1065 |
1066 | return `${text.slice(0, maxLength - 3)}...`;
1067 | }
1068 |
1069 | /**
1070 | * Checks if array or object are empty
1071 | * @param {*} value - The value to check
1072 | * @returns {boolean} True if empty, false otherwise
1073 | */
1074 | function isEmpty(value) {
1075 | if (Array.isArray(value)) {
1076 | return value.length === 0;
1077 | } else if (typeof value === 'object' && value !== null) {
1078 | return Object.keys(value).length === 0;
1079 | }
1080 |
1081 | return false; // Not an array or object, or is null
1082 | }
1083 |
1084 | /**
1085 | * Find cycles in a dependency graph using DFS
1086 | * @param {string} subtaskId - Current subtask ID
1087 | * @param {Map} dependencyMap - Map of subtask IDs to their dependencies
1088 | * @param {Set} visited - Set of visited nodes
1089 | * @param {Set} recursionStack - Set of nodes in current recursion stack
1090 | * @returns {Array} - List of dependency edges that need to be removed to break cycles
1091 | */
1092 | function findCycles(
1093 | subtaskId,
1094 | dependencyMap,
1095 | visited = new Set(),
1096 | recursionStack = new Set(),
1097 | path = []
1098 | ) {
1099 | // Mark the current node as visited and part of recursion stack
1100 | visited.add(subtaskId);
1101 | recursionStack.add(subtaskId);
1102 | path.push(subtaskId);
1103 |
1104 | const cyclesToBreak = [];
1105 |
1106 | // Get all dependencies of the current subtask
1107 | const dependencies = dependencyMap.get(subtaskId) || [];
1108 |
1109 | // For each dependency
1110 | for (const depId of dependencies) {
1111 | // If not visited, recursively check for cycles
1112 | if (!visited.has(depId)) {
1113 | const cycles = findCycles(depId, dependencyMap, visited, recursionStack, [
1114 | ...path
1115 | ]);
1116 | cyclesToBreak.push(...cycles);
1117 | }
1118 | // If the dependency is in the recursion stack, we found a cycle
1119 | else if (recursionStack.has(depId)) {
1120 | // Find the position of the dependency in the path
1121 | const cycleStartIndex = path.indexOf(depId);
1122 | // The last edge in the cycle is what we want to remove
1123 | const cycleEdges = path.slice(cycleStartIndex);
1124 | // We'll remove the last edge in the cycle (the one that points back)
1125 | cyclesToBreak.push(depId);
1126 | }
1127 | }
1128 |
1129 | // Remove the node from recursion stack before returning
1130 | recursionStack.delete(subtaskId);
1131 |
1132 | return cyclesToBreak;
1133 | }
1134 |
1135 | /**
1136 | * Unified dependency traversal utility that supports both forward and reverse dependency traversal
1137 | * @param {Array} sourceTasks - Array of source tasks to start traversal from
1138 | * @param {Array} allTasks - Array of all tasks to search within
1139 | * @param {Object} options - Configuration options
1140 | * @param {number} options.maxDepth - Maximum recursion depth (default: 50)
1141 | * @param {boolean} options.includeSelf - Whether to include self-references (default: false)
1142 | * @param {'forward'|'reverse'} options.direction - Direction of traversal (default: 'forward')
1143 | * @param {Function} options.logger - Optional logger function for warnings
1144 | * @returns {Array} Array of all dependency task IDs found through traversal
1145 | */
1146 | function traverseDependencies(sourceTasks, allTasks, options = {}) {
1147 | const {
1148 | maxDepth = 50,
1149 | includeSelf = false,
1150 | direction = 'forward',
1151 | logger = null
1152 | } = options;
1153 |
1154 | const dependentTaskIds = new Set();
1155 | const processedIds = new Set();
1156 |
1157 | // Helper function to normalize dependency IDs while preserving subtask format
1158 | function normalizeDependencyId(depId) {
1159 | if (typeof depId === 'string') {
1160 | // Preserve string format for subtask IDs like "1.2"
1161 | if (depId.includes('.')) {
1162 | return depId;
1163 | }
1164 | // Convert simple string numbers to numbers for consistency
1165 | const parsed = parseInt(depId, 10);
1166 | return isNaN(parsed) ? depId : parsed;
1167 | }
1168 | return depId;
1169 | }
1170 |
1171 | // Helper function for forward dependency traversal
1172 | function findForwardDependencies(taskId, currentDepth = 0) {
1173 | // Check depth limit
1174 | if (currentDepth >= maxDepth) {
1175 | const warnMsg = `Maximum recursion depth (${maxDepth}) reached for task ${taskId}`;
1176 | if (logger && typeof logger.warn === 'function') {
1177 | logger.warn(warnMsg);
1178 | } else if (typeof log !== 'undefined' && log.warn) {
1179 | log.warn(warnMsg);
1180 | } else {
1181 | console.warn(warnMsg);
1182 | }
1183 | return;
1184 | }
1185 |
1186 | if (processedIds.has(taskId)) {
1187 | return; // Avoid infinite loops
1188 | }
1189 | processedIds.add(taskId);
1190 |
1191 | const task = allTasks.find((t) => t.id === taskId);
1192 | if (!task || !Array.isArray(task.dependencies)) {
1193 | return;
1194 | }
1195 |
1196 | task.dependencies.forEach((depId) => {
1197 | const normalizedDepId = normalizeDependencyId(depId);
1198 |
1199 | // Skip invalid dependencies and optionally skip self-references
1200 | if (
1201 | normalizedDepId == null ||
1202 | (!includeSelf && normalizedDepId === taskId)
1203 | ) {
1204 | return;
1205 | }
1206 |
1207 | dependentTaskIds.add(normalizedDepId);
1208 | // Recursively find dependencies of this dependency
1209 | findForwardDependencies(normalizedDepId, currentDepth + 1);
1210 | });
1211 | }
1212 |
1213 | // Helper function for reverse dependency traversal
1214 | function findReverseDependencies(taskId, currentDepth = 0) {
1215 | // Check depth limit
1216 | if (currentDepth >= maxDepth) {
1217 | const warnMsg = `Maximum recursion depth (${maxDepth}) reached for task ${taskId}`;
1218 | if (logger && typeof logger.warn === 'function') {
1219 | logger.warn(warnMsg);
1220 | } else if (typeof log !== 'undefined' && log.warn) {
1221 | log.warn(warnMsg);
1222 | } else {
1223 | console.warn(warnMsg);
1224 | }
1225 | return;
1226 | }
1227 |
1228 | if (processedIds.has(taskId)) {
1229 | return; // Avoid infinite loops
1230 | }
1231 | processedIds.add(taskId);
1232 |
1233 | allTasks.forEach((task) => {
1234 | if (task.dependencies && Array.isArray(task.dependencies)) {
1235 | const dependsOnTaskId = task.dependencies.some((depId) => {
1236 | const normalizedDepId = normalizeDependencyId(depId);
1237 | return normalizedDepId === taskId;
1238 | });
1239 |
1240 | if (dependsOnTaskId) {
1241 | // Skip invalid dependencies and optionally skip self-references
1242 | if (task.id == null || (!includeSelf && task.id === taskId)) {
1243 | return;
1244 | }
1245 |
1246 | dependentTaskIds.add(task.id);
1247 | // Recursively find tasks that depend on this task
1248 | findReverseDependencies(task.id, currentDepth + 1);
1249 | }
1250 | }
1251 | });
1252 | }
1253 |
1254 | // Choose traversal function based on direction
1255 | const traversalFunc =
1256 | direction === 'reverse' ? findReverseDependencies : findForwardDependencies;
1257 |
1258 | // Start traversal from each source task
1259 | sourceTasks.forEach((sourceTask) => {
1260 | if (sourceTask && sourceTask.id) {
1261 | traversalFunc(sourceTask.id);
1262 | }
1263 | });
1264 |
1265 | return Array.from(dependentTaskIds);
1266 | }
1267 |
1268 | /**
1269 | * Convert a string from camelCase to kebab-case
1270 | * @param {string} str - The string to convert
1271 | * @returns {string} The kebab-case version of the string
1272 | */
1273 | const toKebabCase = (str) => {
1274 | // Special handling for common acronyms
1275 | const withReplacedAcronyms = str
1276 | .replace(/ID/g, 'Id')
1277 | .replace(/API/g, 'Api')
1278 | .replace(/UI/g, 'Ui')
1279 | .replace(/URL/g, 'Url')
1280 | .replace(/URI/g, 'Uri')
1281 | .replace(/JSON/g, 'Json')
1282 | .replace(/XML/g, 'Xml')
1283 | .replace(/HTML/g, 'Html')
1284 | .replace(/CSS/g, 'Css');
1285 |
1286 | // Insert hyphens before capital letters and convert to lowercase
1287 | return withReplacedAcronyms
1288 | .replace(/([A-Z])/g, '-$1')
1289 | .toLowerCase()
1290 | .replace(/^-/, ''); // Remove leading hyphen if present
1291 | };
1292 |
1293 | /**
1294 | * Detect camelCase flags in command arguments
1295 | * @param {string[]} args - Command line arguments to check
1296 | * @returns {Array<{original: string, kebabCase: string}>} - List of flags that should be converted
1297 | */
1298 | function detectCamelCaseFlags(args) {
1299 | const camelCaseFlags = [];
1300 | for (const arg of args) {
1301 | if (arg.startsWith('--')) {
1302 | const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after =
1303 |
1304 | // Skip single-word flags - they can't be camelCase
1305 | if (!flagName.includes('-') && !/[A-Z]/.test(flagName)) {
1306 | continue;
1307 | }
1308 |
1309 | // Check for camelCase pattern (lowercase followed by uppercase)
1310 | if (/[a-z][A-Z]/.test(flagName)) {
1311 | const kebabVersion = toKebabCase(flagName);
1312 | if (kebabVersion !== flagName) {
1313 | camelCaseFlags.push({
1314 | original: flagName,
1315 | kebabCase: kebabVersion
1316 | });
1317 | }
1318 | }
1319 | }
1320 | }
1321 | return camelCaseFlags;
1322 | }
1323 |
1324 | /**
1325 | * Aggregates an array of telemetry objects into a single summary object.
1326 | * @param {Array<Object>} telemetryArray - Array of telemetryData objects.
1327 | * @param {string} overallCommandName - The name for the aggregated command.
1328 | * @returns {Object|null} Aggregated telemetry object or null if input is empty.
1329 | */
1330 | function aggregateTelemetry(telemetryArray, overallCommandName) {
1331 | if (!telemetryArray || telemetryArray.length === 0) {
1332 | return null;
1333 | }
1334 |
1335 | const aggregated = {
1336 | timestamp: new Date().toISOString(), // Use current time for aggregation time
1337 | userId: telemetryArray[0].userId, // Assume userId is consistent
1338 | commandName: overallCommandName,
1339 | modelUsed: 'Multiple', // Default if models vary
1340 | providerName: 'Multiple', // Default if providers vary
1341 | inputTokens: 0,
1342 | outputTokens: 0,
1343 | totalTokens: 0,
1344 | totalCost: 0,
1345 | currency: telemetryArray[0].currency || 'USD' // Assume consistent currency or default
1346 | };
1347 |
1348 | const uniqueModels = new Set();
1349 | const uniqueProviders = new Set();
1350 | const uniqueCurrencies = new Set();
1351 |
1352 | telemetryArray.forEach((item) => {
1353 | aggregated.inputTokens += item.inputTokens || 0;
1354 | aggregated.outputTokens += item.outputTokens || 0;
1355 | aggregated.totalCost += item.totalCost || 0;
1356 | uniqueModels.add(item.modelUsed);
1357 | uniqueProviders.add(item.providerName);
1358 | uniqueCurrencies.add(item.currency || 'USD');
1359 | });
1360 |
1361 | aggregated.totalTokens = aggregated.inputTokens + aggregated.outputTokens;
1362 | aggregated.totalCost = parseFloat(aggregated.totalCost.toFixed(6)); // Fix precision
1363 |
1364 | if (uniqueModels.size === 1) {
1365 | aggregated.modelUsed = [...uniqueModels][0];
1366 | }
1367 | if (uniqueProviders.size === 1) {
1368 | aggregated.providerName = [...uniqueProviders][0];
1369 | }
1370 | if (uniqueCurrencies.size > 1) {
1371 | aggregated.currency = 'Multiple'; // Mark if currencies actually differ
1372 | } else if (uniqueCurrencies.size === 1) {
1373 | aggregated.currency = [...uniqueCurrencies][0];
1374 | }
1375 |
1376 | return aggregated;
1377 | }
1378 |
1379 | /**
1380 | * @deprecated Use TaskMaster.getCurrentTag() instead
1381 | * Gets the current tag from state.json or falls back to defaultTag from config
1382 | * @param {string} projectRoot - The project root directory (required)
1383 | * @returns {string} The current tag name
1384 | */
1385 | function getCurrentTag(projectRoot) {
1386 | if (!projectRoot) {
1387 | throw new Error('projectRoot is required for getCurrentTag');
1388 | }
1389 |
1390 | try {
1391 | // Try to read current tag from state.json using fs directly
1392 | const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
1393 | if (fs.existsSync(statePath)) {
1394 | const rawState = fs.readFileSync(statePath, 'utf8');
1395 | const stateData = JSON.parse(rawState);
1396 | if (stateData && stateData.currentTag) {
1397 | return stateData.currentTag;
1398 | }
1399 | }
1400 | } catch (error) {
1401 | // Ignore errors, fall back to default
1402 | }
1403 |
1404 | // Fall back to defaultTag from config using fs directly
1405 | try {
1406 | const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
1407 | if (fs.existsSync(configPath)) {
1408 | const rawConfig = fs.readFileSync(configPath, 'utf8');
1409 | const configData = JSON.parse(rawConfig);
1410 | if (configData && configData.global && configData.global.defaultTag) {
1411 | return configData.global.defaultTag;
1412 | }
1413 | }
1414 | } catch (error) {
1415 | // Ignore errors, use hardcoded default
1416 | }
1417 |
1418 | // Final fallback
1419 | return 'master';
1420 | }
1421 |
1422 | /**
1423 | * Resolves the tag to use based on options
1424 | * @param {Object} options - Options object
1425 | * @param {string} options.projectRoot - The project root directory (required)
1426 | * @param {string} [options.tag] - Explicit tag to use
1427 | * @returns {string} The resolved tag name
1428 | */
1429 | function resolveTag(options = {}) {
1430 | const { projectRoot, tag } = options;
1431 |
1432 | if (!projectRoot) {
1433 | throw new Error('projectRoot is required for resolveTag');
1434 | }
1435 |
1436 | // If explicit tag provided, use it
1437 | if (tag) {
1438 | return tag;
1439 | }
1440 |
1441 | // Otherwise get current tag from state/config
1442 | return getCurrentTag(projectRoot);
1443 | }
1444 |
1445 | /**
1446 | * Gets the tasks array for a specific tag from tagged tasks.json data
1447 | * @param {Object} data - The parsed tasks.json data (after migration)
1448 | * @param {string} tagName - The tag name to get tasks for
1449 | * @returns {Array} The tasks array for the specified tag, or empty array if not found
1450 | */
1451 | function getTasksForTag(data, tagName) {
1452 | if (!data || !tagName) {
1453 | return [];
1454 | }
1455 |
1456 | // Handle migrated format: { "master": { "tasks": [...] }, "otherTag": { "tasks": [...] } }
1457 | if (
1458 | data[tagName] &&
1459 | data[tagName].tasks &&
1460 | Array.isArray(data[tagName].tasks)
1461 | ) {
1462 | return data[tagName].tasks;
1463 | }
1464 |
1465 | return [];
1466 | }
1467 |
1468 | /**
1469 | * Sets the tasks array for a specific tag in the data structure
1470 | * @param {Object} data - The tasks.json data object
1471 | * @param {string} tagName - The tag name to set tasks for
1472 | * @param {Array} tasks - The tasks array to set
1473 | * @returns {Object} The updated data object
1474 | */
1475 | function setTasksForTag(data, tagName, tasks) {
1476 | if (!data) {
1477 | data = {};
1478 | }
1479 |
1480 | if (!data[tagName]) {
1481 | data[tagName] = {};
1482 | }
1483 |
1484 | data[tagName].tasks = tasks || [];
1485 | return data;
1486 | }
1487 |
1488 | /**
1489 | * Flatten tasks array to include subtasks as individual searchable items
1490 | * @param {Array} tasks - Array of task objects
1491 | * @returns {Array} Flattened array including both tasks and subtasks
1492 | */
1493 | function flattenTasksWithSubtasks(tasks) {
1494 | const flattened = [];
1495 |
1496 | for (const task of tasks) {
1497 | // Add the main task
1498 | flattened.push({
1499 | ...task,
1500 | searchableId: task.id.toString(), // For consistent ID handling
1501 | isSubtask: false
1502 | });
1503 |
1504 | // Add subtasks if they exist
1505 | if (task.subtasks && task.subtasks.length > 0) {
1506 | for (const subtask of task.subtasks) {
1507 | flattened.push({
1508 | ...subtask,
1509 | searchableId: `${task.id}.${subtask.id}`, // Format: "15.2"
1510 | isSubtask: true,
1511 | parentId: task.id,
1512 | parentTitle: task.title,
1513 | // Enhance subtask context with parent information
1514 | title: `${subtask.title} (subtask of: ${task.title})`,
1515 | description: `${subtask.description} [Parent: ${task.description}]`
1516 | });
1517 | }
1518 | }
1519 | }
1520 |
1521 | return flattened;
1522 | }
1523 |
1524 | /**
1525 | * Ensures the tag object has a metadata object with created/updated timestamps.
1526 | * @param {Object} tagObj - The tag object (e.g., data['master'])
1527 | * @param {Object} [opts] - Optional fields (e.g., description, skipUpdate)
1528 | * @param {string} [opts.description] - Description for the tag
1529 | * @param {boolean} [opts.skipUpdate] - If true, don't update the 'updated' timestamp
1530 | * @returns {Object} The updated tag object (for chaining)
1531 | */
1532 | function ensureTagMetadata(tagObj, opts = {}) {
1533 | if (!tagObj || typeof tagObj !== 'object') {
1534 | throw new Error('tagObj must be a valid object');
1535 | }
1536 |
1537 | const now = new Date().toISOString();
1538 |
1539 | if (!tagObj.metadata) {
1540 | // Create new metadata object
1541 | tagObj.metadata = {
1542 | created: now,
1543 | updated: now,
1544 | ...(opts.description ? { description: opts.description } : {})
1545 | };
1546 | } else {
1547 | // Ensure existing metadata has required fields
1548 | if (!tagObj.metadata.created) {
1549 | tagObj.metadata.created = now;
1550 | }
1551 |
1552 | // Update timestamp unless explicitly skipped
1553 | if (!opts.skipUpdate) {
1554 | tagObj.metadata.updated = now;
1555 | }
1556 |
1557 | // Add description if provided and not already present
1558 | if (opts.description && !tagObj.metadata.description) {
1559 | tagObj.metadata.description = opts.description;
1560 | }
1561 | }
1562 |
1563 | return tagObj;
1564 | }
1565 |
1566 | /**
1567 | * Strip ANSI color codes from a string
1568 | * Useful for testing, logging to files, or when clean text output is needed
1569 | * @param {string} text - The text that may contain ANSI color codes
1570 | * @returns {string} - The text with ANSI color codes removed
1571 | */
1572 | function stripAnsiCodes(text) {
1573 | if (typeof text !== 'string') {
1574 | return text;
1575 | }
1576 | // Remove ANSI escape sequences (color codes, cursor movements, etc.)
1577 | return text.replace(/\x1b\[[0-9;]*m/g, '');
1578 | }
1579 |
1580 | // Export all utility functions and configuration
1581 | export {
1582 | LOG_LEVELS,
1583 | log,
1584 | readJSON,
1585 | writeJSON,
1586 | sanitizePrompt,
1587 | readComplexityReport,
1588 | findTaskInComplexityReport,
1589 | taskExists,
1590 | formatTaskId,
1591 | findTaskById,
1592 | truncate,
1593 | isEmpty,
1594 | findCycles,
1595 | traverseDependencies,
1596 | toKebabCase,
1597 | detectCamelCaseFlags,
1598 | disableSilentMode,
1599 | enableSilentMode,
1600 | getTaskManager,
1601 | isSilentMode,
1602 | addComplexityToTask,
1603 | resolveEnvVariable,
1604 | findProjectRoot,
1605 | getTagAwareFilePath,
1606 | slugifyTagForFilePath,
1607 | aggregateTelemetry,
1608 | getCurrentTag,
1609 | resolveTag,
1610 | getTasksForTag,
1611 | setTasksForTag,
1612 | performCompleteTagMigration,
1613 | migrateConfigJson,
1614 | createStateJson,
1615 | markMigrationForNotice,
1616 | flattenTasksWithSubtasks,
1617 | ensureTagMetadata,
1618 | stripAnsiCodes,
1619 | normalizeTaskIds
1620 | };
1621 |
```