This is page 43 of 50. Use http://codebase.md/eyaltoledano/claude-task-master?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
/**
* utils.js
* Utility functions for the Task Master CLI
*/
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import dotenv from 'dotenv';
// Import specific config getters needed here
import { getLogLevel, getDebugFlag } from './config-manager.js';
import * as gitUtils from './utils/git-utils.js';
import {
COMPLEXITY_REPORT_FILE,
LEGACY_COMPLEXITY_REPORT_FILE,
LEGACY_CONFIG_FILE
} from '../../src/constants/paths.js';
// Global silent mode flag
let silentMode = false;
// --- Environment Variable Resolution Utility ---
/**
* Resolves an environment variable's value.
* Precedence:
* 1. session.env (if session provided)
* 2. process.env
* 3. .env file at projectRoot (if projectRoot provided)
* @param {string} key - The environment variable key.
* @param {object|null} [session=null] - The MCP session object.
* @param {string|null} [projectRoot=null] - The project root directory (for .env fallback).
* @returns {string|undefined} The value of the environment variable or undefined if not found.
*/
function resolveEnvVariable(key, session = null, projectRoot = null) {
// 1. Check session.env
if (session?.env?.[key]) {
return session.env[key];
}
// 2. Read .env file at projectRoot
if (projectRoot) {
const envPath = path.join(projectRoot, '.env');
if (fs.existsSync(envPath)) {
try {
const envFileContent = fs.readFileSync(envPath, 'utf-8');
const parsedEnv = dotenv.parse(envFileContent); // Use dotenv to parse
if (parsedEnv && parsedEnv[key]) {
// console.log(`DEBUG: Found key ${key} in ${envPath}`); // Optional debug log
return parsedEnv[key];
}
} catch (error) {
// Log error but don't crash, just proceed as if key wasn't found in file
log('warn', `Could not read or parse ${envPath}: ${error.message}`);
}
}
}
// 3. Fallback: Check process.env
if (process.env[key]) {
return process.env[key];
}
// Not found anywhere
return undefined;
}
// --- Tag-Aware Path Resolution Utility ---
/**
* Slugifies a tag name to be filesystem-safe
* @param {string} tagName - The tag name to slugify
* @returns {string} Slugified tag name safe for filesystem use
*/
function slugifyTagForFilePath(tagName) {
if (!tagName || typeof tagName !== 'string') {
return 'unknown-tag';
}
// Replace invalid filesystem characters with hyphens and clean up
return tagName
.replace(/[^a-zA-Z0-9_-]/g, '-') // Replace invalid chars with hyphens
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
.replace(/-+/g, '-') // Collapse multiple hyphens
.toLowerCase() // Convert to lowercase
.substring(0, 50); // Limit length to prevent overly long filenames
}
/**
* Resolves a file path to be tag-aware, following the pattern used by other commands.
* For non-master tags, appends _slugified-tagname before the file extension.
* @param {string} basePath - The base file path (e.g., '.taskmaster/reports/task-complexity-report.json')
* @param {string|null} tag - The tag name (null, undefined, or 'master' uses base path)
* @param {string} [projectRoot='.'] - The project root directory
* @returns {string} The resolved file path
*/
function getTagAwareFilePath(basePath, tag, projectRoot = '.') {
// Use path.parse and format for clean tag insertion
const parsedPath = path.parse(basePath);
if (!tag || tag === 'master') {
return path.join(projectRoot, basePath);
}
// Slugify the tag for filesystem safety
const slugifiedTag = slugifyTagForFilePath(tag);
// Append slugified tag before file extension
parsedPath.base = `${parsedPath.name}_${slugifiedTag}${parsedPath.ext}`;
const relativePath = path.format(parsedPath);
return path.join(projectRoot, relativePath);
}
// --- Project Root Finding Utility ---
/**
* Recursively searches upwards for project root starting from a given directory.
* @param {string} [startDir=process.cwd()] - The directory to start searching from.
* @param {string[]} [markers=['package.json', '.git', LEGACY_CONFIG_FILE]] - Marker files/dirs to look for.
* @returns {string|null} The path to the project root, or null if not found.
*/
function findProjectRoot(
startDir = process.cwd(),
markers = ['package.json', 'pyproject.toml', '.git', LEGACY_CONFIG_FILE]
) {
let currentPath = path.resolve(startDir);
const rootPath = path.parse(currentPath).root;
while (currentPath !== rootPath) {
// Check if any marker exists in the current directory
const hasMarker = markers.some((marker) => {
const markerPath = path.join(currentPath, marker);
return fs.existsSync(markerPath);
});
if (hasMarker) {
return currentPath;
}
// Move up one directory
currentPath = path.dirname(currentPath);
}
// Check the root directory as well
const hasMarkerInRoot = markers.some((marker) => {
const markerPath = path.join(rootPath, marker);
return fs.existsSync(markerPath);
});
return hasMarkerInRoot ? rootPath : null;
}
// --- Dynamic Configuration Function --- (REMOVED)
// --- Logging and Utility Functions ---
// Set up logging based on log level
const LOG_LEVELS = {
debug: 0,
info: 1,
warn: 2,
error: 3,
success: 1 // Treat success like info level
};
/**
* Returns the task manager module
* @returns {Promise<Object>} The task manager module object
*/
async function getTaskManager() {
return import('./task-manager.js');
}
/**
* Enable silent logging mode
*/
function enableSilentMode() {
silentMode = true;
}
/**
* Disable silent logging mode
*/
function disableSilentMode() {
silentMode = false;
}
/**
* Check if silent mode is enabled
* @returns {boolean} True if silent mode is enabled
*/
function isSilentMode() {
return silentMode;
}
/**
* Logs a message at the specified level
* @param {string} level - The log level (debug, info, warn, error)
* @param {...any} args - Arguments to log
*/
function log(level, ...args) {
// Immediately return if silentMode is enabled
if (isSilentMode()) {
return;
}
// GUARD: Prevent circular dependency during config loading
// Use a simple fallback log level instead of calling getLogLevel()
let configLevel = 'info'; // Default fallback
try {
// Only try to get config level if we're not in the middle of config loading
configLevel = getLogLevel() || 'info';
} catch (error) {
// If getLogLevel() fails (likely due to circular dependency),
// use default 'info' level and continue
configLevel = 'info';
}
// Use text prefixes instead of emojis
const prefixes = {
debug: chalk.gray('[DEBUG]'),
info: chalk.blue('[INFO]'),
warn: chalk.yellow('[WARN]'),
error: chalk.red('[ERROR]'),
success: chalk.green('[SUCCESS]')
};
// Ensure level exists, default to info if not
const currentLevel = LOG_LEVELS.hasOwnProperty(level) ? level : 'info';
// Check log level configuration
if (
LOG_LEVELS[currentLevel] >= (LOG_LEVELS[configLevel] ?? LOG_LEVELS.info)
) {
const prefix = prefixes[currentLevel] || '';
// Use console.log for all levels, let chalk handle coloring
// Construct the message properly
const message = args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg))
.join(' ');
console.log(`${prefix} ${message}`);
}
}
/**
* Checks if the data object has a tagged structure (contains tag objects with tasks arrays)
* @param {Object} data - The data object to check
* @returns {boolean} True if the data has a tagged structure
*/
function hasTaggedStructure(data) {
if (!data || typeof data !== 'object') {
return false;
}
// Check if any top-level properties are objects with tasks arrays
for (const key in data) {
if (
data.hasOwnProperty(key) &&
typeof data[key] === 'object' &&
Array.isArray(data[key].tasks)
) {
return true;
}
}
return false;
}
/**
* Normalizes task IDs to ensure they are numbers instead of strings
* @param {Array} tasks - Array of tasks to normalize
*/
function normalizeTaskIds(tasks) {
if (!Array.isArray(tasks)) return;
tasks.forEach((task) => {
// Convert task ID to number with validation
if (task.id !== undefined) {
const parsedId = parseInt(task.id, 10);
if (!isNaN(parsedId) && parsedId > 0) {
task.id = parsedId;
}
}
// Convert subtask IDs to numbers with validation
if (Array.isArray(task.subtasks)) {
task.subtasks.forEach((subtask) => {
if (subtask.id !== undefined) {
// Check for dot notation (which shouldn't exist in storage)
if (typeof subtask.id === 'string' && subtask.id.includes('.')) {
// Extract the subtask part after the dot
const parts = subtask.id.split('.');
subtask.id = parseInt(parts[parts.length - 1], 10);
} else {
const parsedSubtaskId = parseInt(subtask.id, 10);
if (!isNaN(parsedSubtaskId) && parsedSubtaskId > 0) {
subtask.id = parsedSubtaskId;
}
}
}
});
}
});
}
/**
* Reads and parses a JSON file
* @param {string} filepath - Path to the JSON file
* @param {string} [projectRoot] - Optional project root for tag resolution (used by MCP)
* @param {string} [tag] - Optional tag to use instead of current tag resolution
* @returns {Object|null} The parsed JSON data or null if error
*/
function readJSON(filepath, projectRoot = null, tag = null) {
// GUARD: Prevent circular dependency during config loading
let isDebug = false; // Default fallback
try {
// Only try to get debug flag if we're not in the middle of config loading
isDebug = getDebugFlag();
} catch (error) {
// If getDebugFlag() fails (likely due to circular dependency),
// use default false and continue
}
if (isDebug) {
console.log(
`readJSON called with: ${filepath}, projectRoot: ${projectRoot}, tag: ${tag}`
);
}
if (!filepath) {
return null;
}
let data;
try {
data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
if (isDebug) {
console.log(`Successfully read JSON from ${filepath}`);
}
} catch (err) {
if (isDebug) {
console.log(`Failed to read JSON from ${filepath}: ${err.message}`);
}
return null;
}
// If it's not a tasks.json file, return as-is
if (!filepath.includes('tasks.json') || !data) {
if (isDebug) {
console.log(`File is not tasks.json or data is null, returning as-is`);
}
return data;
}
// Check if this is legacy format that needs migration
// Only migrate if we have tasks at the ROOT level AND no tag-like structure
if (
Array.isArray(data.tasks) &&
!data._rawTaggedData &&
!hasTaggedStructure(data)
) {
if (isDebug) {
console.log(`File is in legacy format, performing migration...`);
}
normalizeTaskIds(data.tasks);
// This is legacy format - migrate it to tagged format
const migratedData = {
master: {
tasks: data.tasks,
metadata: data.metadata || {
created: new Date().toISOString(),
updated: new Date().toISOString(),
description: 'Tasks for master context'
}
}
};
// Write the migrated data back to the file
try {
writeJSON(filepath, migratedData);
if (isDebug) {
console.log(`Successfully migrated legacy format to tagged format`);
}
// Perform complete migration (config.json, state.json)
performCompleteTagMigration(filepath);
// Check and auto-switch git tags if enabled (after migration)
// This needs to run synchronously BEFORE tag resolution
if (projectRoot) {
try {
// Run git integration synchronously
gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath);
} catch (error) {
// Silent fail - don't break normal operations
}
}
// Mark for migration notice
markMigrationForNotice(filepath);
} catch (writeError) {
if (isDebug) {
console.log(`Error writing migrated data: ${writeError.message}`);
}
// If write fails, continue with the original data
}
// Continue processing with the migrated data structure
data = migratedData;
}
// If we have tagged data, we need to resolve which tag to use
if (typeof data === 'object' && !data.tasks) {
// This is tagged format
if (isDebug) {
console.log(`File is in tagged format, resolving tag...`);
}
// Ensure all tags have proper metadata before proceeding
for (const tagName in data) {
if (
data.hasOwnProperty(tagName) &&
typeof data[tagName] === 'object' &&
data[tagName].tasks
) {
try {
ensureTagMetadata(data[tagName], {
description: `Tasks for ${tagName} context`,
skipUpdate: true // Don't update timestamp during read operations
});
} catch (error) {
// If ensureTagMetadata fails, continue without metadata
if (isDebug) {
console.log(
`Failed to ensure metadata for tag ${tagName}: ${error.message}`
);
}
}
}
}
// Store reference to the raw tagged data for functions that need it
const originalTaggedData = JSON.parse(JSON.stringify(data));
// Normalize IDs in all tags before storing as originalTaggedData
for (const tagName in originalTaggedData) {
if (
originalTaggedData[tagName] &&
Array.isArray(originalTaggedData[tagName].tasks)
) {
normalizeTaskIds(originalTaggedData[tagName].tasks);
}
}
// Check and auto-switch git tags if enabled (for existing tagged format)
// This needs to run synchronously BEFORE tag resolution
if (projectRoot) {
try {
// Run git integration synchronously
gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath);
} catch (error) {
// Silent fail - don't break normal operations
}
}
try {
// Default to master tag if anything goes wrong
let resolvedTag = 'master';
// Try to resolve the correct tag, but don't fail if it doesn't work
try {
// If tag is provided, use it directly
if (tag) {
resolvedTag = tag;
} else if (projectRoot) {
// Use provided projectRoot
resolvedTag = resolveTag({ projectRoot });
} else {
// Try to derive projectRoot from filepath
const derivedProjectRoot = findProjectRoot(path.dirname(filepath));
if (derivedProjectRoot) {
resolvedTag = resolveTag({ projectRoot: derivedProjectRoot });
}
// If derivedProjectRoot is null, stick with 'master'
}
} catch (tagResolveError) {
if (isDebug) {
console.log(
`Tag resolution failed, using master: ${tagResolveError.message}`
);
}
// resolvedTag stays as 'master'
}
if (isDebug) {
console.log(`Resolved tag: ${resolvedTag}`);
}
// Get the data for the resolved tag
const tagData = data[resolvedTag];
if (tagData && tagData.tasks) {
normalizeTaskIds(tagData.tasks);
// Add the _rawTaggedData property and the resolved tag to the returned data
const result = {
...tagData,
tag: resolvedTag,
_rawTaggedData: originalTaggedData
};
if (isDebug) {
console.log(
`Returning data for tag '${resolvedTag}' with ${tagData.tasks.length} tasks`
);
}
return result;
} else {
// If the resolved tag doesn't exist, fall back to master
const masterData = data.master;
if (masterData && masterData.tasks) {
normalizeTaskIds(masterData.tasks);
if (isDebug) {
console.log(
`Tag '${resolvedTag}' not found, falling back to master with ${masterData.tasks.length} tasks`
);
}
return {
...masterData,
tag: 'master',
_rawTaggedData: originalTaggedData
};
} else {
if (isDebug) {
console.log(`No valid tag data found, returning empty structure`);
}
// Return empty structure if no valid data
return {
tasks: [],
tag: 'master',
_rawTaggedData: originalTaggedData
};
}
}
} catch (error) {
if (isDebug) {
console.log(`Error during tag resolution: ${error.message}`);
}
// If anything goes wrong, try to return master or empty
const masterData = data.master;
if (masterData && masterData.tasks) {
normalizeTaskIds(masterData.tasks);
return {
...masterData,
_rawTaggedData: originalTaggedData
};
}
return {
tasks: [],
_rawTaggedData: originalTaggedData
};
}
}
// If we reach here, it's some other format
if (isDebug) {
console.log(`File format not recognized, returning as-is`);
}
return data;
}
/**
* Performs complete tag migration including config.json and state.json updates
* @param {string} tasksJsonPath - Path to the tasks.json file that was migrated
*/
function performCompleteTagMigration(tasksJsonPath) {
try {
// Derive project root from tasks.json path
const projectRoot =
findProjectRoot(path.dirname(tasksJsonPath)) ||
path.dirname(tasksJsonPath);
// 1. Migrate config.json - add defaultTag and tags section
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
if (fs.existsSync(configPath)) {
migrateConfigJson(configPath);
}
// 2. Create state.json if it doesn't exist
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (!fs.existsSync(statePath)) {
createStateJson(statePath);
}
if (getDebugFlag()) {
log(
'debug',
`Complete tag migration performed for project: ${projectRoot}`
);
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error during complete tag migration: ${error.message}`);
}
}
}
/**
* Migrates config.json to add tagged task system configuration
* @param {string} configPath - Path to the config.json file
*/
function migrateConfigJson(configPath) {
try {
const rawConfig = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(rawConfig);
if (!config) return;
let modified = false;
// Add global.defaultTag if missing
if (!config.global) {
config.global = {};
}
if (!config.global.defaultTag) {
config.global.defaultTag = 'master';
modified = true;
}
if (modified) {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
console.log(
'[DEBUG] Updated config.json with tagged task system settings'
);
}
}
} catch (error) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(`[WARN] Error migrating config.json: ${error.message}`);
}
}
}
/**
* Creates initial state.json file for tagged task system
* @param {string} statePath - Path where state.json should be created
*/
function createStateJson(statePath) {
try {
const initialState = {
currentTag: 'master',
lastSwitched: new Date().toISOString(),
branchTagMapping: {},
migrationNoticeShown: false
};
fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
console.log('[DEBUG] Created initial state.json for tagged task system');
}
} catch (error) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(`[WARN] Error creating state.json: ${error.message}`);
}
}
}
/**
* Marks in state.json that migration occurred and notice should be shown
* @param {string} tasksJsonPath - Path to the tasks.json file
*/
function markMigrationForNotice(tasksJsonPath) {
try {
const projectRoot = path.dirname(path.dirname(tasksJsonPath));
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
// Ensure state.json exists
if (!fs.existsSync(statePath)) {
createStateJson(statePath);
}
// Read and update state to mark migration occurred using fs directly
try {
const rawState = fs.readFileSync(statePath, 'utf8');
const stateData = JSON.parse(rawState) || {};
// Only set to false if it's not already set (i.e., first time migration)
if (stateData.migrationNoticeShown === undefined) {
stateData.migrationNoticeShown = false;
fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2), 'utf8');
}
} catch (stateError) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(
`[WARN] Error updating state for migration notice: ${stateError.message}`
);
}
}
} catch (error) {
if (process.env.TASKMASTER_DEBUG === 'true') {
console.warn(
`[WARN] Error marking migration for notice: ${error.message}`
);
}
}
}
/**
* Writes and saves a JSON file. Handles tagged task lists properly.
* @param {string} filepath - Path to the JSON file
* @param {Object} data - Data to write (can be resolved tag data or raw tagged data)
* @param {string} projectRoot - Optional project root for tag context
* @param {string} tag - Optional tag for tag context
*/
function writeJSON(filepath, data, projectRoot = null, tag = null) {
const isDebug = process.env.TASKMASTER_DEBUG === 'true';
try {
let finalData = data;
// If data represents resolved tag data but lost _rawTaggedData (edge-case observed in MCP path)
if (
!data._rawTaggedData &&
projectRoot &&
Array.isArray(data.tasks) &&
!hasTaggedStructure(data)
) {
const resolvedTag = tag || getCurrentTag(projectRoot);
if (isDebug) {
console.log(
`writeJSON: Detected resolved tag data missing _rawTaggedData. Re-reading raw data to prevent data loss for tag '${resolvedTag}'.`
);
}
// Re-read the full file to get the complete tagged structure
const rawFullData = JSON.parse(fs.readFileSync(filepath, 'utf8'));
// Merge the updated data into the full structure
finalData = {
...rawFullData,
[resolvedTag]: {
// Preserve existing tag metadata if it exists, otherwise use what's passed
...(rawFullData[resolvedTag]?.metadata || {}),
...(data.metadata ? { metadata: data.metadata } : {}),
tasks: data.tasks // The updated tasks array is the source of truth here
}
};
}
// If we have _rawTaggedData, this means we're working with resolved tag data
// and need to merge it back into the full tagged structure
else if (data && data._rawTaggedData && projectRoot) {
const resolvedTag = tag || getCurrentTag(projectRoot);
// Get the original tagged data
const originalTaggedData = data._rawTaggedData;
// Create a clean copy of the current resolved data (without internal properties)
const { _rawTaggedData, tag: _, ...cleanResolvedData } = data;
// Update the specific tag with the resolved data
finalData = {
...originalTaggedData,
[resolvedTag]: cleanResolvedData
};
if (isDebug) {
console.log(
`writeJSON: Merging resolved data back into tag '${resolvedTag}'`
);
}
}
// Clean up any internal properties that shouldn't be persisted
let cleanData = finalData;
if (cleanData && typeof cleanData === 'object') {
// Remove any _rawTaggedData or tag properties from root level
const { _rawTaggedData, tag: tagProp, ...rootCleanData } = cleanData;
cleanData = rootCleanData;
// Additional cleanup for tag objects
if (typeof cleanData === 'object' && !Array.isArray(cleanData)) {
const finalCleanData = {};
for (const [key, value] of Object.entries(cleanData)) {
if (
value &&
typeof value === 'object' &&
Array.isArray(value.tasks)
) {
// This is a tag object - clean up any rogue root-level properties
const { created, description, ...cleanTagData } = value;
// Only keep the description if there's no metadata.description
if (
description &&
(!cleanTagData.metadata || !cleanTagData.metadata.description)
) {
cleanTagData.description = description;
}
finalCleanData[key] = cleanTagData;
} else {
finalCleanData[key] = value;
}
}
cleanData = finalCleanData;
}
}
fs.writeFileSync(filepath, JSON.stringify(cleanData, null, 2), 'utf8');
if (isDebug) {
console.log(`writeJSON: Successfully wrote to ${filepath}`);
}
} catch (error) {
log('error', `Error writing JSON file ${filepath}:`, error.message);
if (isDebug) {
log('error', 'Full error details:', error);
}
}
}
/**
* Sanitizes a prompt string for use in a shell command
* @param {string} prompt The prompt to sanitize
* @returns {string} Sanitized prompt
*/
function sanitizePrompt(prompt) {
// Replace double quotes with escaped double quotes
return prompt.replace(/"/g, '\\"');
}
/**
* Reads the complexity report from file
* @param {string} customPath - Optional custom path to the report
* @returns {Object|null} The parsed complexity report or null if not found
*/
function readComplexityReport(customPath = null) {
// GUARD: Prevent circular dependency during config loading
let isDebug = false; // Default fallback
try {
// Only try to get debug flag if we're not in the middle of config loading
isDebug = getDebugFlag();
} catch (error) {
// If getDebugFlag() fails (likely due to circular dependency),
// use default false and continue
isDebug = false;
}
try {
let reportPath;
if (customPath) {
reportPath = customPath;
} else {
// Try new location first, then fall back to legacy
const newPath = path.join(process.cwd(), COMPLEXITY_REPORT_FILE);
const legacyPath = path.join(
process.cwd(),
LEGACY_COMPLEXITY_REPORT_FILE
);
reportPath = fs.existsSync(newPath) ? newPath : legacyPath;
}
if (!fs.existsSync(reportPath)) {
if (isDebug) {
log('debug', `Complexity report not found at ${reportPath}`);
}
return null;
}
const reportData = readJSON(reportPath);
if (isDebug) {
log('debug', `Successfully read complexity report from ${reportPath}`);
}
return reportData;
} catch (error) {
if (isDebug) {
log('error', `Error reading complexity report: ${error.message}`);
}
return null;
}
}
/**
* Finds a task analysis in the complexity report
* @param {Object} report - The complexity report
* @param {number} taskId - The task ID to find
* @returns {Object|null} The task analysis or null if not found
*/
function findTaskInComplexityReport(report, taskId) {
if (
!report ||
!report.complexityAnalysis ||
!Array.isArray(report.complexityAnalysis)
) {
return null;
}
return report.complexityAnalysis.find((task) => task.taskId === taskId);
}
function addComplexityToTask(task, complexityReport) {
let taskId;
if (task.isSubtask) {
taskId = task.parentTask.id;
} else if (task.parentId) {
taskId = task.parentId;
} else {
taskId = task.id;
}
const taskAnalysis = findTaskInComplexityReport(complexityReport, taskId);
if (taskAnalysis) {
task.complexityScore = taskAnalysis.complexityScore;
}
}
/**
* Checks if a task exists in the tasks array
* @param {Array} tasks - The tasks array
* @param {string|number} taskId - The task ID to check
* @returns {boolean} True if the task exists, false otherwise
*/
function taskExists(tasks, taskId) {
if (!taskId || !tasks || !Array.isArray(tasks)) {
return false;
}
// Handle both regular task IDs and subtask IDs (e.g., "1.2")
if (typeof taskId === 'string' && taskId.includes('.')) {
const [parentId, subtaskId] = taskId
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = tasks.find((t) => t.id === parentId);
if (!parentTask || !parentTask.subtasks) {
return false;
}
return parentTask.subtasks.some((st) => st.id === subtaskId);
}
const id = parseInt(taskId, 10);
return tasks.some((t) => t.id === id);
}
/**
* Formats a task ID as a string
* @param {string|number} id - The task ID to format
* @returns {string} The formatted task ID
*/
function formatTaskId(id) {
if (typeof id === 'string' && id.includes('.')) {
return id; // Already formatted as a string with a dot (e.g., "1.2")
}
if (typeof id === 'number') {
return id.toString();
}
return id;
}
/**
* Finds a task by ID in the tasks array. Optionally filters subtasks by status.
* @param {Array} tasks - The tasks array
* @param {string|number} taskId - The task ID to find
* @param {Object|null} complexityReport - Optional pre-loaded complexity report
* @param {string} [statusFilter] - Optional status to filter subtasks by
* @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.
*/
function findTaskById(
tasks,
taskId,
complexityReport = null,
statusFilter = null
) {
if (!taskId || !tasks || !Array.isArray(tasks)) {
return { task: null, originalSubtaskCount: null };
}
// Check if it's a subtask ID (e.g., "1.2")
if (typeof taskId === 'string' && taskId.includes('.')) {
// If looking for a subtask, statusFilter doesn't apply directly here.
const [parentId, subtaskId] = taskId
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = tasks.find((t) => t.id === parentId);
if (!parentTask || !parentTask.subtasks) {
return { task: null, originalSubtaskCount: null, originalSubtasks: null };
}
const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
if (subtask) {
// Add reference to parent task for context
subtask.parentTask = {
id: parentTask.id,
title: parentTask.title,
status: parentTask.status
};
subtask.isSubtask = true;
}
// If we found a task, check for complexity data
if (subtask && complexityReport) {
addComplexityToTask(subtask, complexityReport);
}
return {
task: subtask || null,
originalSubtaskCount: null,
originalSubtasks: null
};
}
let taskResult = null;
let originalSubtaskCount = null;
let originalSubtasks = null;
// Find the main task
const id = parseInt(taskId, 10);
const task = tasks.find((t) => t.id === id) || null;
// If task not found, return nulls
if (!task) {
return { task: null, originalSubtaskCount: null, originalSubtasks: null };
}
taskResult = task;
// If task found and statusFilter provided, filter its subtasks
if (statusFilter && task.subtasks && Array.isArray(task.subtasks)) {
// Store original subtasks and count before filtering
originalSubtasks = [...task.subtasks]; // Clone the original subtasks array
originalSubtaskCount = task.subtasks.length;
// Clone the task to avoid modifying the original array
const filteredTask = { ...task };
filteredTask.subtasks = task.subtasks.filter(
(subtask) =>
subtask.status &&
subtask.status.toLowerCase() === statusFilter.toLowerCase()
);
taskResult = filteredTask;
}
// If task found and complexityReport provided, add complexity data
if (taskResult && complexityReport) {
addComplexityToTask(taskResult, complexityReport);
}
// Return the found task, original subtask count, and original subtasks
return { task: taskResult, originalSubtaskCount, originalSubtasks };
}
/**
* Truncates text to a specified length
* @param {string} text - The text to truncate
* @param {number} maxLength - The maximum length
* @returns {string} The truncated text
*/
function truncate(text, maxLength) {
if (!text || text.length <= maxLength) {
return text;
}
return `${text.slice(0, maxLength - 3)}...`;
}
/**
* Checks if array or object are empty
* @param {*} value - The value to check
* @returns {boolean} True if empty, false otherwise
*/
function isEmpty(value) {
if (Array.isArray(value)) {
return value.length === 0;
} else if (typeof value === 'object' && value !== null) {
return Object.keys(value).length === 0;
}
return false; // Not an array or object, or is null
}
/**
* Find cycles in a dependency graph using DFS
* @param {string} subtaskId - Current subtask ID
* @param {Map} dependencyMap - Map of subtask IDs to their dependencies
* @param {Set} visited - Set of visited nodes
* @param {Set} recursionStack - Set of nodes in current recursion stack
* @returns {Array} - List of dependency edges that need to be removed to break cycles
*/
function findCycles(
subtaskId,
dependencyMap,
visited = new Set(),
recursionStack = new Set(),
path = []
) {
// Mark the current node as visited and part of recursion stack
visited.add(subtaskId);
recursionStack.add(subtaskId);
path.push(subtaskId);
const cyclesToBreak = [];
// Get all dependencies of the current subtask
const dependencies = dependencyMap.get(subtaskId) || [];
// For each dependency
for (const depId of dependencies) {
// If not visited, recursively check for cycles
if (!visited.has(depId)) {
const cycles = findCycles(depId, dependencyMap, visited, recursionStack, [
...path
]);
cyclesToBreak.push(...cycles);
}
// If the dependency is in the recursion stack, we found a cycle
else if (recursionStack.has(depId)) {
// Find the position of the dependency in the path
const cycleStartIndex = path.indexOf(depId);
// The last edge in the cycle is what we want to remove
const cycleEdges = path.slice(cycleStartIndex);
// We'll remove the last edge in the cycle (the one that points back)
cyclesToBreak.push(depId);
}
}
// Remove the node from recursion stack before returning
recursionStack.delete(subtaskId);
return cyclesToBreak;
}
/**
* Unified dependency traversal utility that supports both forward and reverse dependency traversal
* @param {Array} sourceTasks - Array of source tasks to start traversal from
* @param {Array} allTasks - Array of all tasks to search within
* @param {Object} options - Configuration options
* @param {number} options.maxDepth - Maximum recursion depth (default: 50)
* @param {boolean} options.includeSelf - Whether to include self-references (default: false)
* @param {'forward'|'reverse'} options.direction - Direction of traversal (default: 'forward')
* @param {Function} options.logger - Optional logger function for warnings
* @returns {Array} Array of all dependency task IDs found through traversal
*/
function traverseDependencies(sourceTasks, allTasks, options = {}) {
const {
maxDepth = 50,
includeSelf = false,
direction = 'forward',
logger = null
} = options;
const dependentTaskIds = new Set();
const processedIds = new Set();
// Helper function to normalize dependency IDs while preserving subtask format
function normalizeDependencyId(depId) {
if (typeof depId === 'string') {
// Preserve string format for subtask IDs like "1.2"
if (depId.includes('.')) {
return depId;
}
// Convert simple string numbers to numbers for consistency
const parsed = parseInt(depId, 10);
return isNaN(parsed) ? depId : parsed;
}
return depId;
}
// Helper function for forward dependency traversal
function findForwardDependencies(taskId, currentDepth = 0) {
// Check depth limit
if (currentDepth >= maxDepth) {
const warnMsg = `Maximum recursion depth (${maxDepth}) reached for task ${taskId}`;
if (logger && typeof logger.warn === 'function') {
logger.warn(warnMsg);
} else if (typeof log !== 'undefined' && log.warn) {
log.warn(warnMsg);
} else {
console.warn(warnMsg);
}
return;
}
if (processedIds.has(taskId)) {
return; // Avoid infinite loops
}
processedIds.add(taskId);
const task = allTasks.find((t) => t.id === taskId);
if (!task || !Array.isArray(task.dependencies)) {
return;
}
task.dependencies.forEach((depId) => {
const normalizedDepId = normalizeDependencyId(depId);
// Skip invalid dependencies and optionally skip self-references
if (
normalizedDepId == null ||
(!includeSelf && normalizedDepId === taskId)
) {
return;
}
dependentTaskIds.add(normalizedDepId);
// Recursively find dependencies of this dependency
findForwardDependencies(normalizedDepId, currentDepth + 1);
});
}
// Helper function for reverse dependency traversal
function findReverseDependencies(taskId, currentDepth = 0) {
// Check depth limit
if (currentDepth >= maxDepth) {
const warnMsg = `Maximum recursion depth (${maxDepth}) reached for task ${taskId}`;
if (logger && typeof logger.warn === 'function') {
logger.warn(warnMsg);
} else if (typeof log !== 'undefined' && log.warn) {
log.warn(warnMsg);
} else {
console.warn(warnMsg);
}
return;
}
if (processedIds.has(taskId)) {
return; // Avoid infinite loops
}
processedIds.add(taskId);
allTasks.forEach((task) => {
if (task.dependencies && Array.isArray(task.dependencies)) {
const dependsOnTaskId = task.dependencies.some((depId) => {
const normalizedDepId = normalizeDependencyId(depId);
return normalizedDepId === taskId;
});
if (dependsOnTaskId) {
// Skip invalid dependencies and optionally skip self-references
if (task.id == null || (!includeSelf && task.id === taskId)) {
return;
}
dependentTaskIds.add(task.id);
// Recursively find tasks that depend on this task
findReverseDependencies(task.id, currentDepth + 1);
}
}
});
}
// Choose traversal function based on direction
const traversalFunc =
direction === 'reverse' ? findReverseDependencies : findForwardDependencies;
// Start traversal from each source task
sourceTasks.forEach((sourceTask) => {
if (sourceTask && sourceTask.id) {
traversalFunc(sourceTask.id);
}
});
return Array.from(dependentTaskIds);
}
/**
* Convert a string from camelCase to kebab-case
* @param {string} str - The string to convert
* @returns {string} The kebab-case version of the string
*/
const toKebabCase = (str) => {
// Special handling for common acronyms
const withReplacedAcronyms = str
.replace(/ID/g, 'Id')
.replace(/API/g, 'Api')
.replace(/UI/g, 'Ui')
.replace(/URL/g, 'Url')
.replace(/URI/g, 'Uri')
.replace(/JSON/g, 'Json')
.replace(/XML/g, 'Xml')
.replace(/HTML/g, 'Html')
.replace(/CSS/g, 'Css');
// Insert hyphens before capital letters and convert to lowercase
return withReplacedAcronyms
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
.replace(/^-/, ''); // Remove leading hyphen if present
};
/**
* Detect camelCase flags in command arguments
* @param {string[]} args - Command line arguments to check
* @returns {Array<{original: string, kebabCase: string}>} - List of flags that should be converted
*/
function detectCamelCaseFlags(args) {
const camelCaseFlags = [];
for (const arg of args) {
if (arg.startsWith('--')) {
const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after =
// Skip single-word flags - they can't be camelCase
if (!flagName.includes('-') && !/[A-Z]/.test(flagName)) {
continue;
}
// Check for camelCase pattern (lowercase followed by uppercase)
if (/[a-z][A-Z]/.test(flagName)) {
const kebabVersion = toKebabCase(flagName);
if (kebabVersion !== flagName) {
camelCaseFlags.push({
original: flagName,
kebabCase: kebabVersion
});
}
}
}
}
return camelCaseFlags;
}
/**
* Aggregates an array of telemetry objects into a single summary object.
* @param {Array<Object>} telemetryArray - Array of telemetryData objects.
* @param {string} overallCommandName - The name for the aggregated command.
* @returns {Object|null} Aggregated telemetry object or null if input is empty.
*/
function aggregateTelemetry(telemetryArray, overallCommandName) {
if (!telemetryArray || telemetryArray.length === 0) {
return null;
}
const aggregated = {
timestamp: new Date().toISOString(), // Use current time for aggregation time
userId: telemetryArray[0].userId, // Assume userId is consistent
commandName: overallCommandName,
modelUsed: 'Multiple', // Default if models vary
providerName: 'Multiple', // Default if providers vary
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
totalCost: 0,
currency: telemetryArray[0].currency || 'USD' // Assume consistent currency or default
};
const uniqueModels = new Set();
const uniqueProviders = new Set();
const uniqueCurrencies = new Set();
telemetryArray.forEach((item) => {
aggregated.inputTokens += item.inputTokens || 0;
aggregated.outputTokens += item.outputTokens || 0;
aggregated.totalCost += item.totalCost || 0;
uniqueModels.add(item.modelUsed);
uniqueProviders.add(item.providerName);
uniqueCurrencies.add(item.currency || 'USD');
});
aggregated.totalTokens = aggregated.inputTokens + aggregated.outputTokens;
aggregated.totalCost = parseFloat(aggregated.totalCost.toFixed(6)); // Fix precision
if (uniqueModels.size === 1) {
aggregated.modelUsed = [...uniqueModels][0];
}
if (uniqueProviders.size === 1) {
aggregated.providerName = [...uniqueProviders][0];
}
if (uniqueCurrencies.size > 1) {
aggregated.currency = 'Multiple'; // Mark if currencies actually differ
} else if (uniqueCurrencies.size === 1) {
aggregated.currency = [...uniqueCurrencies][0];
}
return aggregated;
}
/**
* @deprecated Use TaskMaster.getCurrentTag() instead
* Gets the current tag from state.json or falls back to defaultTag from config
* @param {string} projectRoot - The project root directory (required)
* @returns {string} The current tag name
*/
function getCurrentTag(projectRoot) {
if (!projectRoot) {
throw new Error('projectRoot is required for getCurrentTag');
}
try {
// Try to read current tag from state.json using fs directly
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
if (fs.existsSync(statePath)) {
const rawState = fs.readFileSync(statePath, 'utf8');
const stateData = JSON.parse(rawState);
if (stateData && stateData.currentTag) {
return stateData.currentTag;
}
}
} catch (error) {
// Ignore errors, fall back to default
}
// Fall back to defaultTag from config using fs directly
try {
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
if (fs.existsSync(configPath)) {
const rawConfig = fs.readFileSync(configPath, 'utf8');
const configData = JSON.parse(rawConfig);
if (configData && configData.global && configData.global.defaultTag) {
return configData.global.defaultTag;
}
}
} catch (error) {
// Ignore errors, use hardcoded default
}
// Final fallback
return 'master';
}
/**
* Resolves the tag to use based on options
* @param {Object} options - Options object
* @param {string} options.projectRoot - The project root directory (required)
* @param {string} [options.tag] - Explicit tag to use
* @returns {string} The resolved tag name
*/
function resolveTag(options = {}) {
const { projectRoot, tag } = options;
if (!projectRoot) {
throw new Error('projectRoot is required for resolveTag');
}
// If explicit tag provided, use it
if (tag) {
return tag;
}
// Otherwise get current tag from state/config
return getCurrentTag(projectRoot);
}
/**
* Gets the tasks array for a specific tag from tagged tasks.json data
* @param {Object} data - The parsed tasks.json data (after migration)
* @param {string} tagName - The tag name to get tasks for
* @returns {Array} The tasks array for the specified tag, or empty array if not found
*/
function getTasksForTag(data, tagName) {
if (!data || !tagName) {
return [];
}
// Handle migrated format: { "master": { "tasks": [...] }, "otherTag": { "tasks": [...] } }
if (
data[tagName] &&
data[tagName].tasks &&
Array.isArray(data[tagName].tasks)
) {
return data[tagName].tasks;
}
return [];
}
/**
* Sets the tasks array for a specific tag in the data structure
* @param {Object} data - The tasks.json data object
* @param {string} tagName - The tag name to set tasks for
* @param {Array} tasks - The tasks array to set
* @returns {Object} The updated data object
*/
function setTasksForTag(data, tagName, tasks) {
if (!data) {
data = {};
}
if (!data[tagName]) {
data[tagName] = {};
}
data[tagName].tasks = tasks || [];
return data;
}
/**
* Flatten tasks array to include subtasks as individual searchable items
* @param {Array} tasks - Array of task objects
* @returns {Array} Flattened array including both tasks and subtasks
*/
function flattenTasksWithSubtasks(tasks) {
const flattened = [];
for (const task of tasks) {
// Add the main task
flattened.push({
...task,
searchableId: task.id.toString(), // For consistent ID handling
isSubtask: false
});
// Add subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
for (const subtask of task.subtasks) {
flattened.push({
...subtask,
searchableId: `${task.id}.${subtask.id}`, // Format: "15.2"
isSubtask: true,
parentId: task.id,
parentTitle: task.title,
// Enhance subtask context with parent information
title: `${subtask.title} (subtask of: ${task.title})`,
description: `${subtask.description} [Parent: ${task.description}]`
});
}
}
}
return flattened;
}
/**
* Ensures the tag object has a metadata object with created/updated timestamps.
* @param {Object} tagObj - The tag object (e.g., data['master'])
* @param {Object} [opts] - Optional fields (e.g., description, skipUpdate)
* @param {string} [opts.description] - Description for the tag
* @param {boolean} [opts.skipUpdate] - If true, don't update the 'updated' timestamp
* @returns {Object} The updated tag object (for chaining)
*/
function ensureTagMetadata(tagObj, opts = {}) {
if (!tagObj || typeof tagObj !== 'object') {
throw new Error('tagObj must be a valid object');
}
const now = new Date().toISOString();
if (!tagObj.metadata) {
// Create new metadata object
tagObj.metadata = {
created: now,
updated: now,
...(opts.description ? { description: opts.description } : {})
};
} else {
// Ensure existing metadata has required fields
if (!tagObj.metadata.created) {
tagObj.metadata.created = now;
}
// Update timestamp unless explicitly skipped
if (!opts.skipUpdate) {
tagObj.metadata.updated = now;
}
// Add description if provided and not already present
if (opts.description && !tagObj.metadata.description) {
tagObj.metadata.description = opts.description;
}
}
return tagObj;
}
/**
* Strip ANSI color codes from a string
* Useful for testing, logging to files, or when clean text output is needed
* @param {string} text - The text that may contain ANSI color codes
* @returns {string} - The text with ANSI color codes removed
*/
function stripAnsiCodes(text) {
if (typeof text !== 'string') {
return text;
}
// Remove ANSI escape sequences (color codes, cursor movements, etc.)
return text.replace(/\x1b\[[0-9;]*m/g, '');
}
// Export all utility functions and configuration
export {
LOG_LEVELS,
log,
readJSON,
writeJSON,
sanitizePrompt,
readComplexityReport,
findTaskInComplexityReport,
taskExists,
formatTaskId,
findTaskById,
truncate,
isEmpty,
findCycles,
traverseDependencies,
toKebabCase,
detectCamelCaseFlags,
disableSilentMode,
enableSilentMode,
getTaskManager,
isSilentMode,
addComplexityToTask,
resolveEnvVariable,
findProjectRoot,
getTagAwareFilePath,
slugifyTagForFilePath,
aggregateTelemetry,
getCurrentTag,
resolveTag,
getTasksForTag,
setTasksForTag,
performCompleteTagMigration,
migrateConfigJson,
createStateJson,
markMigrationForNotice,
flattenTasksWithSubtasks,
ensureTagMetadata,
stripAnsiCodes,
normalizeTaskIds
};
```
--------------------------------------------------------------------------------
/scripts/modules/dependency-manager.js:
--------------------------------------------------------------------------------
```javascript
/**
* dependency-manager.js
* Manages task dependencies and relationships
*/
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import {
log,
readJSON,
writeJSON,
taskExists,
formatTaskId,
findCycles,
traverseDependencies,
isSilentMode
} from './utils.js';
import { displayBanner } from './ui.js';
import generateTaskFiles from './task-manager/generate-task-files.js';
/**
* Structured error class for dependency operations
*/
class DependencyError extends Error {
constructor(code, message, data = {}) {
super(message);
this.name = 'DependencyError';
this.code = code;
this.data = data;
}
}
/**
* Error codes for dependency operations
*/
const DEPENDENCY_ERROR_CODES = {
CANNOT_MOVE_SUBTASK: 'CANNOT_MOVE_SUBTASK',
INVALID_TASK_ID: 'INVALID_TASK_ID',
INVALID_SOURCE_TAG: 'INVALID_SOURCE_TAG',
INVALID_TARGET_TAG: 'INVALID_TARGET_TAG'
};
/**
* Add a dependency to a task
* @param {string} tasksPath - Path to the tasks.json file
* @param {number|string} taskId - ID of the task to add dependency to
* @param {number|string} dependencyId - ID of the task to add as dependency
* @param {Object} context - Context object containing projectRoot and tag information
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
*/
async function addDependency(tasksPath, taskId, dependencyId, context = {}) {
log('info', `Adding dependency ${dependencyId} to task ${taskId}...`);
const data = readJSON(tasksPath, context.projectRoot, context.tag);
if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json');
process.exit(1);
}
// Format the task and dependency IDs correctly
const formattedTaskId =
typeof taskId === 'string' && taskId.includes('.')
? taskId
: parseInt(taskId, 10);
const formattedDependencyId = formatTaskId(dependencyId);
// Check if the dependency task or subtask actually exists
if (!taskExists(data.tasks, formattedDependencyId)) {
log(
'error',
`Dependency target ${formattedDependencyId} does not exist in tasks.json`
);
process.exit(1);
}
// Find the task to update
let targetTask = null;
let isSubtask = false;
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
// Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) {
log('error', `Parent task ${parentId} not found.`);
process.exit(1);
}
if (!parentTask.subtasks) {
log('error', `Parent task ${parentId} has no subtasks.`);
process.exit(1);
}
targetTask = parentTask.subtasks.find((s) => s.id === subtaskId);
isSubtask = true;
if (!targetTask) {
log('error', `Subtask ${formattedTaskId} not found.`);
process.exit(1);
}
} else {
// Regular task (not a subtask)
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
if (!targetTask) {
log('error', `Task ${formattedTaskId} not found.`);
process.exit(1);
}
}
// Initialize dependencies array if it doesn't exist
if (!targetTask.dependencies) {
targetTask.dependencies = [];
}
// Check if dependency already exists
if (
targetTask.dependencies.some((d) => {
// Convert both to strings for comparison to handle both numeric and string IDs
return String(d) === String(formattedDependencyId);
})
) {
log(
'warn',
`Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.`
);
return;
}
// Check if the task is trying to depend on itself - compare full IDs (including subtask parts)
if (String(formattedTaskId) === String(formattedDependencyId)) {
log('error', `Task ${formattedTaskId} cannot depend on itself.`);
process.exit(1);
}
// For subtasks of the same parent, we need to make sure we're not treating it as a self-dependency
// Check if we're dealing with subtasks with the same parent task
let isSelfDependency = false;
if (
typeof formattedTaskId === 'string' &&
typeof formattedDependencyId === 'string' &&
formattedTaskId.includes('.') &&
formattedDependencyId.includes('.')
) {
const [taskParentId] = formattedTaskId.split('.');
const [depParentId] = formattedDependencyId.split('.');
// Only treat it as a self-dependency if both the parent ID and subtask ID are identical
isSelfDependency = formattedTaskId === formattedDependencyId;
// Log for debugging
log(
'debug',
`Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}`
);
log(
'debug',
`Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}`
);
}
if (isSelfDependency) {
log('error', `Subtask ${formattedTaskId} cannot depend on itself.`);
process.exit(1);
}
// Check for circular dependencies
const dependencyChain = [formattedTaskId];
if (
!isCircularDependency(data.tasks, formattedDependencyId, dependencyChain)
) {
// Add the dependency
targetTask.dependencies.push(formattedDependencyId);
// Sort dependencies numerically or by parent task ID first, then subtask ID
targetTask.dependencies.sort((a, b) => {
if (typeof a === 'number' && typeof b === 'number') {
return a - b;
} else if (typeof a === 'string' && typeof b === 'string') {
const [aParent, aChild] = a.split('.').map(Number);
const [bParent, bChild] = b.split('.').map(Number);
return aParent !== bParent ? aParent - bParent : aChild - bChild;
} else if (typeof a === 'number') {
return -1; // Numbers come before strings
} else {
return 1; // Strings come after numbers
}
});
// Save changes
writeJSON(tasksPath, data, context.projectRoot, context.tag);
log(
'success',
`Added dependency ${formattedDependencyId} to task ${formattedTaskId}`
);
// Display a more visually appealing success message
if (!isSilentMode()) {
console.log(
boxen(
chalk.green(`Successfully added dependency:\n\n`) +
`Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
}
// Generate updated task files
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
log('info', 'Task files regenerated with updated dependencies.');
} else {
log(
'error',
`Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`
);
process.exit(1);
}
}
/**
* Remove a dependency from a task
* @param {string} tasksPath - Path to the tasks.json file
* @param {number|string} taskId - ID of the task to remove dependency from
* @param {number|string} dependencyId - ID of the task to remove as dependency
* @param {Object} context - Context object containing projectRoot and tag information
* @param {string} [context.projectRoot] - Project root path
* @param {string} [context.tag] - Tag for the task
*/
async function removeDependency(tasksPath, taskId, dependencyId, context = {}) {
log('info', `Removing dependency ${dependencyId} from task ${taskId}...`);
// Read tasks file
const data = readJSON(tasksPath, context.projectRoot, context.tag);
if (!data || !data.tasks) {
log('error', 'No valid tasks found.');
process.exit(1);
}
// Format the task and dependency IDs correctly
const formattedTaskId =
typeof taskId === 'string' && taskId.includes('.')
? taskId
: parseInt(taskId, 10);
const formattedDependencyId = formatTaskId(dependencyId);
// Find the task to update
let targetTask = null;
let isSubtask = false;
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
// Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) {
log('error', `Parent task ${parentId} not found.`);
process.exit(1);
}
if (!parentTask.subtasks) {
log('error', `Parent task ${parentId} has no subtasks.`);
process.exit(1);
}
targetTask = parentTask.subtasks.find((s) => s.id === subtaskId);
isSubtask = true;
if (!targetTask) {
log('error', `Subtask ${formattedTaskId} not found.`);
process.exit(1);
}
} else {
// Regular task (not a subtask)
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
if (!targetTask) {
log('error', `Task ${formattedTaskId} not found.`);
process.exit(1);
}
}
// Check if the task has any dependencies
if (!targetTask.dependencies || targetTask.dependencies.length === 0) {
log(
'info',
`Task ${formattedTaskId} has no dependencies, nothing to remove.`
);
return;
}
// Normalize the dependency ID for comparison to handle different formats
const normalizedDependencyId = String(formattedDependencyId);
// Check if the dependency exists by comparing string representations
const dependencyIndex = targetTask.dependencies.findIndex((dep) => {
// Direct string comparison (handles both numeric IDs and dot notation)
const depStr = String(dep);
if (depStr === normalizedDependencyId) {
return true;
}
// For subtasks: handle numeric dependencies that might be references to other subtasks
// in the same parent (e.g., subtask 1.2 depending on subtask 1.1 stored as just "1")
if (typeof dep === 'number' && dep < 100 && isSubtask) {
const [parentId] = formattedTaskId.split('.');
const fullSubtaskRef = `${parentId}.${dep}`;
if (fullSubtaskRef === normalizedDependencyId) {
return true;
}
}
return false;
});
if (dependencyIndex === -1) {
log(
'info',
`Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`
);
return;
}
// Remove the dependency
targetTask.dependencies.splice(dependencyIndex, 1);
// Save the updated tasks
writeJSON(tasksPath, data, context.projectRoot, context.tag);
// Success message
log(
'success',
`Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}`
);
if (!isSilentMode()) {
// Display a more visually appealing success message
console.log(
boxen(
chalk.green(`Successfully removed dependency:\n\n`) +
`Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
}
// Regenerate task files
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
}
/**
* Check if adding a dependency would create a circular dependency
* @param {Array} tasks - Array of all tasks
* @param {number|string} taskId - ID of task to check
* @param {Array} chain - Chain of dependencies to check
* @returns {boolean} True if circular dependency would be created
*/
function isCircularDependency(tasks, taskId, chain = []) {
// Convert taskId to string for comparison
const taskIdStr = String(taskId);
// If we've seen this task before in the chain, we have a circular dependency
if (chain.some((id) => String(id) === taskIdStr)) {
return true;
}
// Find the task or subtask
let task = null;
let parentIdForSubtask = null;
// Check if this is a subtask reference (e.g., "1.2")
if (taskIdStr.includes('.')) {
const [parentId, subtaskId] = taskIdStr.split('.').map(Number);
const parentTask = tasks.find((t) => t.id === parentId);
parentIdForSubtask = parentId; // Store parent ID if it's a subtask
if (parentTask && parentTask.subtasks) {
task = parentTask.subtasks.find((st) => st.id === subtaskId);
}
} else {
// Regular task - handle both string and numeric task IDs
const taskIdNum = parseInt(taskIdStr, 10);
task = tasks.find((t) => t.id === taskIdNum || String(t.id) === taskIdStr);
}
if (!task) {
return false; // Task doesn't exist, can't create circular dependency
}
// No dependencies, can't create circular dependency
if (!task.dependencies || task.dependencies.length === 0) {
return false;
}
// Check each dependency recursively
const newChain = [...chain, taskIdStr]; // Use taskIdStr for consistency
return task.dependencies.some((depId) => {
let normalizedDepId = String(depId);
// Normalize relative subtask dependencies
if (typeof depId === 'number' && parentIdForSubtask !== null) {
// If the current task is a subtask AND the dependency is a number,
// assume it refers to a sibling subtask.
normalizedDepId = `${parentIdForSubtask}.${depId}`;
}
// Pass the normalized ID to the recursive call
return isCircularDependency(tasks, normalizedDepId, newChain);
});
}
/**
* Validate task dependencies
* @param {Array} tasks - Array of all tasks
* @returns {Object} Validation result with valid flag and issues array
*/
function validateTaskDependencies(tasks) {
const issues = [];
// Check each task's dependencies
tasks.forEach((task) => {
if (!task.dependencies) {
return; // No dependencies to validate
}
task.dependencies.forEach((depId) => {
// Check for self-dependencies
if (String(depId) === String(task.id)) {
issues.push({
type: 'self',
taskId: task.id,
message: `Task ${task.id} depends on itself`
});
return;
}
// Check if dependency exists
if (!taskExists(tasks, depId)) {
issues.push({
type: 'missing',
taskId: task.id,
dependencyId: depId,
message: `Task ${task.id} depends on non-existent task ${depId}`
});
}
});
// Check for circular dependencies
if (isCircularDependency(tasks, task.id)) {
issues.push({
type: 'circular',
taskId: task.id,
message: `Task ${task.id} is part of a circular dependency chain`
});
}
// Check subtask dependencies if they exist
if (task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach((subtask) => {
if (!subtask.dependencies) {
return; // No dependencies to validate
}
// Create a full subtask ID for reference
const fullSubtaskId = `${task.id}.${subtask.id}`;
subtask.dependencies.forEach((depId) => {
// Check for self-dependencies in subtasks
if (
String(depId) === String(fullSubtaskId) ||
(typeof depId === 'number' && depId === subtask.id)
) {
issues.push({
type: 'self',
taskId: fullSubtaskId,
message: `Subtask ${fullSubtaskId} depends on itself`
});
return;
}
// Check if dependency exists
if (!taskExists(tasks, depId)) {
issues.push({
type: 'missing',
taskId: fullSubtaskId,
dependencyId: depId,
message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`
});
}
});
// Check for circular dependencies in subtasks
if (isCircularDependency(tasks, fullSubtaskId)) {
issues.push({
type: 'circular',
taskId: fullSubtaskId,
message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`
});
}
});
}
});
return {
valid: issues.length === 0,
issues
};
}
/**
* Remove duplicate dependencies from tasks
* @param {Object} tasksData - Tasks data object with tasks array
* @returns {Object} Updated tasks data with duplicates removed
*/
function removeDuplicateDependencies(tasksData) {
const tasks = tasksData.tasks.map((task) => {
if (!task.dependencies) {
return task;
}
// Convert to Set and back to array to remove duplicates
const uniqueDeps = [...new Set(task.dependencies)];
return {
...task,
dependencies: uniqueDeps
};
});
return {
...tasksData,
tasks
};
}
/**
* Clean up invalid subtask dependencies
* @param {Object} tasksData - Tasks data object with tasks array
* @returns {Object} Updated tasks data with invalid subtask dependencies removed
*/
function cleanupSubtaskDependencies(tasksData) {
const tasks = tasksData.tasks.map((task) => {
// Handle task's own dependencies
if (task.dependencies) {
task.dependencies = task.dependencies.filter((depId) => {
// Keep only dependencies that exist
return taskExists(tasksData.tasks, depId);
});
}
// Handle subtask dependencies
if (task.subtasks) {
task.subtasks = task.subtasks.map((subtask) => {
if (!subtask.dependencies) {
return subtask;
}
// Filter out dependencies to non-existent subtasks
subtask.dependencies = subtask.dependencies.filter((depId) => {
return taskExists(tasksData.tasks, depId);
});
return subtask;
});
}
return task;
});
return {
...tasksData,
tasks
};
}
/**
* Validate dependencies in task files
* @param {string} tasksPath - Path to tasks.json
* @param {Object} options - Options object, including context
*/
async function validateDependenciesCommand(tasksPath, options = {}) {
const { context = {} } = options;
log('info', 'Checking for invalid dependencies in task files...');
// Read tasks data
const data = readJSON(tasksPath, context.projectRoot, context.tag);
if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json');
process.exit(1);
}
// Count of tasks and subtasks for reporting
const taskCount = data.tasks.length;
let subtaskCount = 0;
data.tasks.forEach((task) => {
if (task.subtasks && Array.isArray(task.subtasks)) {
subtaskCount += task.subtasks.length;
}
});
log(
'info',
`Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...`
);
try {
// Directly call the validation function
const validationResult = validateTaskDependencies(data.tasks);
if (!validationResult.valid) {
log(
'error',
`Dependency validation failed. Found ${validationResult.issues.length} issue(s):`
);
validationResult.issues.forEach((issue) => {
let errorMsg = ` [${issue.type.toUpperCase()}] Task ${issue.taskId}: ${issue.message}`;
if (issue.dependencyId) {
errorMsg += ` (Dependency: ${issue.dependencyId})`;
}
log('error', errorMsg); // Log each issue as an error
});
// Optionally exit if validation fails, depending on desired behavior
// process.exit(1); // Uncomment if validation failure should stop the process
// Display summary box even on failure, showing issues found
if (!isSilentMode()) {
console.log(
boxen(
chalk.red(`Dependency Validation FAILED\n\n`) +
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
`${chalk.red('Issues found:')} ${validationResult.issues.length}`, // Display count from result
{
padding: 1,
borderColor: 'red',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
}
} else {
log(
'success',
'No invalid dependencies found - all dependencies are valid'
);
// Show validation summary - only if not in silent mode
if (!isSilentMode()) {
console.log(
boxen(
chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
}
}
} catch (error) {
log('error', 'Error validating dependencies:', error);
process.exit(1);
}
}
/**
* Helper function to count all dependencies across tasks and subtasks
* @param {Array} tasks - All tasks
* @returns {number} - Total number of dependencies
*/
function countAllDependencies(tasks) {
let count = 0;
tasks.forEach((task) => {
// Count main task dependencies
if (task.dependencies && Array.isArray(task.dependencies)) {
count += task.dependencies.length;
}
// Count subtask dependencies
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach((subtask) => {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
count += subtask.dependencies.length;
}
});
}
});
return count;
}
/**
* Fixes invalid dependencies in tasks.json
* @param {string} tasksPath - Path to tasks.json
* @param {Object} options - Options object, including context
*/
async function fixDependenciesCommand(tasksPath, options = {}) {
const { context = {} } = options;
log('info', 'Checking for and fixing invalid dependencies in tasks.json...');
try {
// Read tasks data
const data = readJSON(tasksPath, context.projectRoot, context.tag);
if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json');
process.exit(1);
}
// Create a deep copy of the original data for comparison
const originalData = JSON.parse(JSON.stringify(data));
// Track fixes for reporting
const stats = {
nonExistentDependenciesRemoved: 0,
selfDependenciesRemoved: 0,
duplicateDependenciesRemoved: 0,
circularDependenciesFixed: 0,
tasksFixed: 0,
subtasksFixed: 0
};
// First phase: Remove duplicate dependencies in tasks
data.tasks.forEach((task) => {
if (task.dependencies && Array.isArray(task.dependencies)) {
const uniqueDeps = new Set();
const originalLength = task.dependencies.length;
task.dependencies = task.dependencies.filter((depId) => {
const depIdStr = String(depId);
if (uniqueDeps.has(depIdStr)) {
log(
'info',
`Removing duplicate dependency from task ${task.id}: ${depId}`
);
stats.duplicateDependenciesRemoved++;
return false;
}
uniqueDeps.add(depIdStr);
return true;
});
if (task.dependencies.length < originalLength) {
stats.tasksFixed++;
}
}
// Check for duplicates in subtasks
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach((subtask) => {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const uniqueDeps = new Set();
const originalLength = subtask.dependencies.length;
subtask.dependencies = subtask.dependencies.filter((depId) => {
let depIdStr = String(depId);
if (typeof depId === 'number' && depId < 100) {
depIdStr = `${task.id}.${depId}`;
}
if (uniqueDeps.has(depIdStr)) {
log(
'info',
`Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`
);
stats.duplicateDependenciesRemoved++;
return false;
}
uniqueDeps.add(depIdStr);
return true;
});
if (subtask.dependencies.length < originalLength) {
stats.subtasksFixed++;
}
}
});
}
});
// Create validity maps for tasks and subtasks
const validTaskIds = new Set(data.tasks.map((t) => t.id));
const validSubtaskIds = new Set();
data.tasks.forEach((task) => {
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach((subtask) => {
validSubtaskIds.add(`${task.id}.${subtask.id}`);
});
}
});
// Second phase: Remove invalid task dependencies (non-existent tasks)
data.tasks.forEach((task) => {
if (task.dependencies && Array.isArray(task.dependencies)) {
const originalLength = task.dependencies.length;
task.dependencies = task.dependencies.filter((depId) => {
const isSubtask = typeof depId === 'string' && depId.includes('.');
if (isSubtask) {
// Check if the subtask exists
if (!validSubtaskIds.has(depId)) {
log(
'info',
`Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`
);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
} else {
// Check if the task exists
const numericId =
typeof depId === 'string' ? parseInt(depId, 10) : depId;
if (!validTaskIds.has(numericId)) {
log(
'info',
`Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`
);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
}
});
if (task.dependencies.length < originalLength) {
stats.tasksFixed++;
}
}
// Check subtask dependencies for invalid references
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach((subtask) => {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const originalLength = subtask.dependencies.length;
const subtaskId = `${task.id}.${subtask.id}`;
// First check for self-dependencies
const hasSelfDependency = subtask.dependencies.some((depId) => {
if (typeof depId === 'string' && depId.includes('.')) {
return depId === subtaskId;
} else if (typeof depId === 'number' && depId < 100) {
return depId === subtask.id;
}
return false;
});
if (hasSelfDependency) {
subtask.dependencies = subtask.dependencies.filter((depId) => {
const normalizedDepId =
typeof depId === 'number' && depId < 100
? `${task.id}.${depId}`
: String(depId);
if (normalizedDepId === subtaskId) {
log(
'info',
`Removing self-dependency from subtask ${subtaskId}`
);
stats.selfDependenciesRemoved++;
return false;
}
return true;
});
}
// Then check for non-existent dependencies
subtask.dependencies = subtask.dependencies.filter((depId) => {
if (typeof depId === 'string' && depId.includes('.')) {
if (!validSubtaskIds.has(depId)) {
log(
'info',
`Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)`
);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
}
// Handle numeric dependencies
const numericId =
typeof depId === 'number' ? depId : parseInt(depId, 10);
// Small numbers likely refer to subtasks in the same task
if (numericId < 100) {
const fullSubtaskId = `${task.id}.${numericId}`;
if (!validSubtaskIds.has(fullSubtaskId)) {
log(
'info',
`Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}`
);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
}
// Otherwise it's a task reference
if (!validTaskIds.has(numericId)) {
log(
'info',
`Removing invalid task dependency from subtask ${subtaskId}: ${numericId}`
);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
});
if (subtask.dependencies.length < originalLength) {
stats.subtasksFixed++;
}
}
});
}
});
// Third phase: Check for circular dependencies
log('info', 'Checking for circular dependencies...');
// Build the dependency map for subtasks
const subtaskDependencyMap = new Map();
data.tasks.forEach((task) => {
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach((subtask) => {
const subtaskId = `${task.id}.${subtask.id}`;
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const normalizedDeps = subtask.dependencies.map((depId) => {
if (typeof depId === 'string' && depId.includes('.')) {
return depId;
} else if (typeof depId === 'number' && depId < 100) {
return `${task.id}.${depId}`;
}
return String(depId);
});
subtaskDependencyMap.set(subtaskId, normalizedDeps);
} else {
subtaskDependencyMap.set(subtaskId, []);
}
});
}
});
// Check for and fix circular dependencies
for (const [subtaskId, dependencies] of subtaskDependencyMap.entries()) {
const visited = new Set();
const recursionStack = new Set();
// Detect cycles
const cycleEdges = findCycles(
subtaskId,
subtaskDependencyMap,
visited,
recursionStack
);
if (cycleEdges.length > 0) {
const [taskId, subtaskNum] = subtaskId
.split('.')
.map((part) => Number(part));
const task = data.tasks.find((t) => t.id === taskId);
if (task && task.subtasks) {
const subtask = task.subtasks.find((st) => st.id === subtaskNum);
if (subtask && subtask.dependencies) {
const originalLength = subtask.dependencies.length;
const edgesToRemove = cycleEdges.map((edge) => {
if (edge.includes('.')) {
const [depTaskId, depSubtaskId] = edge
.split('.')
.map((part) => Number(part));
if (depTaskId === taskId) {
return depSubtaskId;
}
return edge;
}
return Number(edge);
});
subtask.dependencies = subtask.dependencies.filter((depId) => {
const normalizedDepId =
typeof depId === 'number' && depId < 100
? `${taskId}.${depId}`
: String(depId);
if (
edgesToRemove.includes(depId) ||
edgesToRemove.includes(normalizedDepId)
) {
log(
'info',
`Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}`
);
stats.circularDependenciesFixed++;
return false;
}
return true;
});
if (subtask.dependencies.length < originalLength) {
stats.subtasksFixed++;
}
}
}
}
}
// Check if any changes were made by comparing with original data
const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData);
if (dataChanged) {
// Save the changes
writeJSON(tasksPath, data, context.projectRoot, context.tag);
log('success', 'Fixed dependency issues in tasks.json');
// Regenerate task files
log('info', 'Regenerating task files to reflect dependency changes...');
// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
} else {
log('info', 'No changes needed to fix dependencies');
}
// Show detailed statistics report
const totalFixedAll =
stats.nonExistentDependenciesRemoved +
stats.selfDependenciesRemoved +
stats.duplicateDependenciesRemoved +
stats.circularDependenciesFixed;
if (!isSilentMode()) {
if (totalFixedAll > 0) {
log('success', `Fixed ${totalFixedAll} dependency issues in total!`);
console.log(
boxen(
chalk.green(`Dependency Fixes Summary:\n\n`) +
`${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` +
`${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` +
`${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` +
`${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` +
`${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` +
`${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
} else {
log(
'success',
'No dependency issues found - all dependencies are valid'
);
console.log(
boxen(
chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
}
}
} catch (error) {
log('error', 'Error in fix-dependencies command:', error);
process.exit(1);
}
}
/**
* Ensure at least one subtask in each task has no dependencies
* @param {Object} tasksData - The tasks data object with tasks array
* @returns {boolean} - True if any changes were made
*/
function ensureAtLeastOneIndependentSubtask(tasksData) {
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
return false;
}
let changesDetected = false;
tasksData.tasks.forEach((task) => {
if (
!task.subtasks ||
!Array.isArray(task.subtasks) ||
task.subtasks.length === 0
) {
return;
}
// Check if any subtask has no dependencies
const hasIndependentSubtask = task.subtasks.some(
(st) =>
!st.dependencies ||
!Array.isArray(st.dependencies) ||
st.dependencies.length === 0
);
if (!hasIndependentSubtask) {
// Find the first subtask and clear its dependencies
if (task.subtasks.length > 0) {
const firstSubtask = task.subtasks[0];
log(
'debug',
`Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}`
);
firstSubtask.dependencies = [];
changesDetected = true;
}
}
});
return changesDetected;
}
/**
* Validate and fix dependencies across all tasks and subtasks
* This function is designed to be called after any task modification
* @param {Object} tasksData - The tasks data object with tasks array
* @param {string} tasksPath - Optional path to save the changes
* @param {string} projectRoot - Optional project root for tag context
* @param {string} tag - Optional tag for tag context
* @returns {boolean} - True if any changes were made
*/
function validateAndFixDependencies(
tasksData,
tasksPath = null,
projectRoot = null,
tag = null
) {
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
log('error', 'Invalid tasks data');
return false;
}
log('debug', 'Validating and fixing dependencies...');
// Create a deep copy for comparison
const originalData = JSON.parse(JSON.stringify(tasksData));
// 1. Remove duplicate dependencies from tasks and subtasks
tasksData.tasks = tasksData.tasks.map((task) => {
// Handle task dependencies
if (task.dependencies) {
const uniqueDeps = [...new Set(task.dependencies)];
task.dependencies = uniqueDeps;
}
// Handle subtask dependencies
if (task.subtasks) {
task.subtasks = task.subtasks.map((subtask) => {
if (subtask.dependencies) {
const uniqueDeps = [...new Set(subtask.dependencies)];
subtask.dependencies = uniqueDeps;
}
return subtask;
});
}
return task;
});
// 2. Remove invalid task dependencies (non-existent tasks)
tasksData.tasks.forEach((task) => {
// Clean up task dependencies
if (task.dependencies) {
task.dependencies = task.dependencies.filter((depId) => {
// Remove self-dependencies
if (String(depId) === String(task.id)) {
return false;
}
// Remove non-existent dependencies
return taskExists(tasksData.tasks, depId);
});
}
// Clean up subtask dependencies
if (task.subtasks) {
task.subtasks.forEach((subtask) => {
if (subtask.dependencies) {
subtask.dependencies = subtask.dependencies.filter((depId) => {
// Handle numeric subtask references
if (typeof depId === 'number' && depId < 100) {
const fullSubtaskId = `${task.id}.${depId}`;
return taskExists(tasksData.tasks, fullSubtaskId);
}
// Handle full task/subtask references
return taskExists(tasksData.tasks, depId);
});
}
});
}
});
// 3. Ensure at least one subtask has no dependencies in each task
tasksData.tasks.forEach((task) => {
if (task.subtasks && task.subtasks.length > 0) {
const hasIndependentSubtask = task.subtasks.some(
(st) =>
!st.dependencies ||
!Array.isArray(st.dependencies) ||
st.dependencies.length === 0
);
if (!hasIndependentSubtask) {
task.subtasks[0].dependencies = [];
}
}
});
// Check if any changes were made by comparing with original data
const changesDetected =
JSON.stringify(tasksData) !== JSON.stringify(originalData);
// Save changes if needed
if (tasksPath && changesDetected) {
try {
writeJSON(tasksPath, tasksData, projectRoot, tag);
log('debug', 'Saved dependency fixes to tasks.json');
} catch (error) {
log('error', 'Failed to save dependency fixes to tasks.json', error);
}
}
return changesDetected;
}
/**
* Recursively find all dependencies for a set of tasks with depth limiting
* Recursively find all dependencies for a set of tasks with depth limiting
*
* @note This function depends on the traverseDependencies utility from utils.js
* for the actual dependency traversal logic.
*
* @param {Array} sourceTasks - Array of source tasks to find dependencies for
* @param {Array} allTasks - Array of all available tasks
* @param {Object} options - Options object
* @param {number} options.maxDepth - Maximum recursion depth (default: 50)
* @param {boolean} options.includeSelf - Whether to include self-references (default: false)
* @returns {Array} Array of all dependency task IDs
*/
function findAllDependenciesRecursively(sourceTasks, allTasks, options = {}) {
if (!Array.isArray(sourceTasks)) {
throw new Error('Source tasks parameter must be an array');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
return traverseDependencies(sourceTasks, allTasks, {
...options,
direction: 'forward',
logger: { warn: log.warn || console.warn }
});
}
/**
* Find dependency task by ID, handling various ID formats
* @param {string|number} depId - Dependency ID to find
* @param {string} taskId - ID of the task that has this dependency
* @param {Array} allTasks - Array of all tasks to search
* @returns {Object|null} Found dependency task or null
*/
/**
* Find a subtask within a parent task's subtasks array
* @param {string} parentId - The parent task ID
* @param {string|number} subtaskId - The subtask ID to find
* @param {Array} allTasks - Array of all tasks to search in
* @param {boolean} useStringComparison - Whether to use string comparison for subtaskId
* @returns {Object|null} The found subtask with full ID or null if not found
*/
function findSubtaskInParent(
parentId,
subtaskId,
allTasks,
useStringComparison = false
) {
// Convert parentId to numeric for proper comparison with top-level task IDs
const numericParentId = parseInt(parentId, 10);
const parentTask = allTasks.find((t) => t.id === numericParentId);
if (parentTask && parentTask.subtasks && Array.isArray(parentTask.subtasks)) {
const foundSubtask = parentTask.subtasks.find((subtask) =>
useStringComparison
? String(subtask.id) === String(subtaskId)
: subtask.id === subtaskId
);
if (foundSubtask) {
// Return a task-like object that represents the subtask with full ID
return {
...foundSubtask,
id: `${parentId}.${foundSubtask.id}`
};
}
}
return null;
}
function findDependencyTask(depId, taskId, allTasks) {
if (!depId) {
return null;
}
// Convert depId to string for consistent comparison
const depIdStr = String(depId);
// Find the dependency task - handle both top-level and subtask IDs
let depTask = null;
// First try exact match (for top-level tasks)
depTask = allTasks.find((t) => String(t.id) === depIdStr);
// If not found and it's a subtask reference (contains dot), find the parent task first
if (!depTask && depIdStr.includes('.')) {
const [parentId, subtaskId] = depIdStr.split('.');
depTask = findSubtaskInParent(parentId, subtaskId, allTasks, true);
}
// If still not found, try numeric comparison for relative subtask references
if (!depTask && !isNaN(depId)) {
const numericId = parseInt(depId, 10);
// For subtasks, this might be a relative reference within the same parent
if (taskId && typeof taskId === 'string' && taskId.includes('.')) {
const [parentId] = taskId.split('.');
depTask = findSubtaskInParent(parentId, numericId, allTasks, false);
}
}
return depTask;
}
/**
* Check if a task has cross-tag dependencies
* @param {Object} task - Task to check
* @param {string} targetTag - Target tag name
* @param {Array} allTasks - Array of all tasks from all tags
* @returns {Array} Array of cross-tag dependency conflicts
*/
function findTaskCrossTagConflicts(task, targetTag, allTasks) {
const conflicts = [];
// Validate task.dependencies is an array before processing
if (!Array.isArray(task.dependencies) || task.dependencies.length === 0) {
return conflicts;
}
// Filter out null/undefined dependencies and check each valid dependency
const validDependencies = task.dependencies.filter((depId) => depId != null);
validDependencies.forEach((depId) => {
const depTask = findDependencyTask(depId, task.id, allTasks);
if (depTask && depTask.tag !== targetTag) {
conflicts.push({
taskId: task.id,
dependencyId: depId,
dependencyTag: depTask.tag,
message: `Task ${task.id} depends on ${depId} (in ${depTask.tag})`
});
}
});
return conflicts;
}
function validateCrossTagMove(task, sourceTag, targetTag, allTasks) {
// Parameter validation
if (!task || typeof task !== 'object') {
throw new Error('Task parameter must be a valid object');
}
if (!sourceTag || typeof sourceTag !== 'string') {
throw new Error('Source tag must be a valid string');
}
if (!targetTag || typeof targetTag !== 'string') {
throw new Error('Target tag must be a valid string');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
const conflicts = findTaskCrossTagConflicts(task, targetTag, allTasks);
return {
canMove: conflicts.length === 0,
conflicts
};
}
/**
* Find all cross-tag dependencies for a set of tasks
* @param {Array} sourceTasks - Array of tasks to check
* @param {string} sourceTag - Source tag name
* @param {string} targetTag - Target tag name
* @param {Array} allTasks - Array of all tasks from all tags
* @returns {Array} Array of cross-tag dependency conflicts
*/
function findCrossTagDependencies(sourceTasks, sourceTag, targetTag, allTasks) {
// Parameter validation
if (!Array.isArray(sourceTasks)) {
throw new Error('Source tasks parameter must be an array');
}
if (!sourceTag || typeof sourceTag !== 'string') {
throw new Error('Source tag must be a valid string');
}
if (!targetTag || typeof targetTag !== 'string') {
throw new Error('Target tag must be a valid string');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
const conflicts = [];
sourceTasks.forEach((task) => {
// Validate task object and dependencies array
if (
!task ||
typeof task !== 'object' ||
!Array.isArray(task.dependencies) ||
task.dependencies.length === 0
) {
return;
}
// Use the shared helper function to find conflicts for this task
const taskConflicts = findTaskCrossTagConflicts(task, targetTag, allTasks);
conflicts.push(...taskConflicts);
});
return conflicts;
}
/**
* Helper function to find all tasks that depend on a given task (reverse dependencies)
* @param {string|number} taskId - The task ID to find dependencies for
* @param {Array} allTasks - Array of all tasks to search
* @param {Set} dependentTaskIds - Set to add found dependencies to
*/
function findTasksThatDependOn(taskId, allTasks, dependentTaskIds) {
// Find the task object for the given ID
const sourceTask = allTasks.find((t) => t.id === taskId);
if (!sourceTask) {
return;
}
// Use the shared utility for reverse dependency traversal
const reverseDeps = traverseDependencies([sourceTask], allTasks, {
direction: 'reverse',
includeSelf: false,
logger: { warn: log.warn || console.warn }
});
// Add all found reverse dependencies to the dependentTaskIds set
reverseDeps.forEach((depId) => dependentTaskIds.add(depId));
}
/**
* Helper function to check if a task depends on a source task
* @param {Object} task - Task to check for dependencies
* @param {Object} sourceTask - Source task to check dependency against
* @returns {boolean} True if task depends on source task
*/
function taskDependsOnSource(task, sourceTask) {
if (!task || !Array.isArray(task.dependencies)) {
return false;
}
const sourceTaskIdStr = String(sourceTask.id);
return task.dependencies.some((depId) => {
if (!depId) return false;
const depIdStr = String(depId);
// Exact match
if (depIdStr === sourceTaskIdStr) {
return true;
}
// Handle subtask references
if (
sourceTaskIdStr &&
typeof sourceTaskIdStr === 'string' &&
sourceTaskIdStr.includes('.')
) {
// If source is a subtask, check if dependency references the parent
const [parentId] = sourceTaskIdStr.split('.');
if (depIdStr === parentId) {
return true;
}
}
// Handle relative subtask references
if (
depIdStr &&
typeof depIdStr === 'string' &&
depIdStr.includes('.') &&
sourceTaskIdStr &&
typeof sourceTaskIdStr === 'string' &&
sourceTaskIdStr.includes('.')
) {
const [depParentId] = depIdStr.split('.');
const [sourceParentId] = sourceTaskIdStr.split('.');
if (depParentId === sourceParentId) {
// Both are subtasks of the same parent, check if they reference each other
const depSubtaskNum = parseInt(depIdStr.split('.')[1], 10);
const sourceSubtaskNum = parseInt(sourceTaskIdStr.split('.')[1], 10);
if (depSubtaskNum === sourceSubtaskNum) {
return true;
}
}
}
return false;
});
}
/**
* Helper function to check if any subtasks of a task depend on source tasks
* @param {Object} task - Task to check subtasks of
* @param {Array} sourceTasks - Array of source tasks to check dependencies against
* @returns {boolean} True if any subtasks depend on source tasks
*/
function subtasksDependOnSource(task, sourceTasks) {
if (!task.subtasks || !Array.isArray(task.subtasks)) {
return false;
}
return task.subtasks.some((subtask) => {
// Check if this subtask depends on any source task
const subtaskDependsOnSource = sourceTasks.some((sourceTask) =>
taskDependsOnSource(subtask, sourceTask)
);
if (subtaskDependsOnSource) {
return true;
}
// Recursively check if any nested subtasks depend on source tasks
if (subtask.subtasks && Array.isArray(subtask.subtasks)) {
return subtasksDependOnSource(subtask, sourceTasks);
}
return false;
});
}
/**
* Get all dependent task IDs for a set of cross-tag dependencies
* @param {Array} sourceTasks - Array of source tasks
* @param {Array} crossTagDependencies - Array of cross-tag dependency conflicts
* @param {Array} allTasks - Array of all tasks from all tags
* @returns {Array} Array of dependent task IDs to move
*/
function getDependentTaskIds(sourceTasks, crossTagDependencies, allTasks) {
// Enhanced parameter validation
if (!Array.isArray(sourceTasks)) {
throw new Error('Source tasks parameter must be an array');
}
if (!Array.isArray(crossTagDependencies)) {
throw new Error('Cross tag dependencies parameter must be an array');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
// Use the shared recursive dependency finder
const dependentTaskIds = new Set(
findAllDependenciesRecursively(sourceTasks, allTasks, {
includeSelf: false
})
);
// Add immediate dependency IDs from conflicts and find their dependencies recursively
const conflictTasksToProcess = [];
crossTagDependencies.forEach((conflict) => {
if (conflict && conflict.dependencyId) {
const depId =
typeof conflict.dependencyId === 'string'
? parseInt(conflict.dependencyId, 10)
: conflict.dependencyId;
if (!isNaN(depId)) {
dependentTaskIds.add(depId);
// Find the task object for recursive dependency finding
const depTask = allTasks.find((t) => t.id === depId);
if (depTask) {
conflictTasksToProcess.push(depTask);
}
}
}
});
// Find dependencies of conflict tasks
if (conflictTasksToProcess.length > 0) {
const conflictDependencies = findAllDependenciesRecursively(
conflictTasksToProcess,
allTasks,
{ includeSelf: false }
);
conflictDependencies.forEach((depId) => dependentTaskIds.add(depId));
}
// For --with-dependencies, we also need to find all dependencies of the source tasks
sourceTasks.forEach((sourceTask) => {
if (sourceTask && sourceTask.id) {
// Find all tasks that this source task depends on (forward dependencies) - already handled above
// Find all tasks that depend on this source task (reverse dependencies)
findTasksThatDependOn(sourceTask.id, allTasks, dependentTaskIds);
}
});
// Also include any tasks that depend on the source tasks
sourceTasks.forEach((sourceTask) => {
if (!sourceTask || typeof sourceTask !== 'object' || !sourceTask.id) {
return; // Skip invalid source tasks
}
allTasks.forEach((task) => {
// Validate task and dependencies array
if (
!task ||
typeof task !== 'object' ||
!Array.isArray(task.dependencies)
) {
return;
}
// Check if this task depends on the source task
const hasDependency = taskDependsOnSource(task, sourceTask);
// Check if any subtasks of this task depend on the source task
const subtasksHaveDependency = subtasksDependOnSource(task, [sourceTask]);
if (hasDependency || subtasksHaveDependency) {
dependentTaskIds.add(task.id);
}
});
});
return Array.from(dependentTaskIds);
}
/**
* Validate subtask movement - block direct cross-tag subtask moves
* @param {string} taskId - Task ID to validate
* @param {string} sourceTag - Source tag name
* @param {string} targetTag - Target tag name
* @throws {Error} If subtask movement is attempted
*/
function validateSubtaskMove(taskId, sourceTag, targetTag) {
// Parameter validation
if (!taskId || typeof taskId !== 'string') {
throw new DependencyError(
DEPENDENCY_ERROR_CODES.INVALID_TASK_ID,
'Task ID must be a valid string'
);
}
if (!sourceTag || typeof sourceTag !== 'string') {
throw new DependencyError(
DEPENDENCY_ERROR_CODES.INVALID_SOURCE_TAG,
'Source tag must be a valid string'
);
}
if (!targetTag || typeof targetTag !== 'string') {
throw new DependencyError(
DEPENDENCY_ERROR_CODES.INVALID_TARGET_TAG,
'Target tag must be a valid string'
);
}
if (taskId.includes('.')) {
throw new DependencyError(
DEPENDENCY_ERROR_CODES.CANNOT_MOVE_SUBTASK,
`Cannot move subtask ${taskId} directly between tags.
First promote it to a full task using:
task-master remove-subtask --id=${taskId} --convert`,
{
taskId,
sourceTag,
targetTag
}
);
}
}
/**
* Check if a task can be moved with its dependencies
* @param {string} taskId - Task ID to check
* @param {string} sourceTag - Source tag name
* @param {string} targetTag - Target tag name
* @param {Array} allTasks - Array of all tasks from all tags
* @returns {Object} Object with canMove boolean and dependentTaskIds array
*/
function canMoveWithDependencies(taskId, sourceTag, targetTag, allTasks) {
// Parameter validation
if (!taskId || typeof taskId !== 'string') {
throw new Error('Task ID must be a valid string');
}
if (!sourceTag || typeof sourceTag !== 'string') {
throw new Error('Source tag must be a valid string');
}
if (!targetTag || typeof targetTag !== 'string') {
throw new Error('Target tag must be a valid string');
}
if (!Array.isArray(allTasks)) {
throw new Error('All tasks parameter must be an array');
}
// Enhanced task lookup to handle subtasks properly
let sourceTask = null;
// Check if it's a subtask ID (e.g., "1.2")
if (taskId.includes('.')) {
const [parentId, subtaskId] = taskId
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = allTasks.find(
(t) => t.id === parentId && t.tag === sourceTag
);
if (
parentTask &&
parentTask.subtasks &&
Array.isArray(parentTask.subtasks)
) {
const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
if (subtask) {
// Create a copy of the subtask with parent context
sourceTask = {
...subtask,
parentTask: {
id: parentTask.id,
title: parentTask.title,
status: parentTask.status
},
isSubtask: true
};
}
}
} else {
// Regular task lookup - handle both string and numeric IDs
sourceTask = allTasks.find((t) => {
const taskIdNum = parseInt(taskId, 10);
return (t.id === taskIdNum || t.id === taskId) && t.tag === sourceTag;
});
}
if (!sourceTask) {
return {
canMove: false,
dependentTaskIds: [],
conflicts: [],
error: 'Task not found'
};
}
const validation = validateCrossTagMove(
sourceTask,
sourceTag,
targetTag,
allTasks
);
// Fix contradictory logic: return canMove: false when conflicts exist
if (validation.canMove) {
return {
canMove: true,
dependentTaskIds: [],
conflicts: []
};
}
// When conflicts exist, return canMove: false with conflicts and dependent task IDs
const dependentTaskIds = getDependentTaskIds(
[sourceTask],
validation.conflicts,
allTasks
);
return {
canMove: false,
dependentTaskIds,
conflicts: validation.conflicts
};
}
export {
addDependency,
removeDependency,
isCircularDependency,
validateTaskDependencies,
validateDependenciesCommand,
fixDependenciesCommand,
removeDuplicateDependencies,
cleanupSubtaskDependencies,
ensureAtLeastOneIndependentSubtask,
validateAndFixDependencies,
findDependencyTask,
findTaskCrossTagConflicts,
validateCrossTagMove,
findCrossTagDependencies,
getDependentTaskIds,
validateSubtaskMove,
canMoveWithDependencies,
findAllDependenciesRecursively,
DependencyError,
DEPENDENCY_ERROR_CODES
};
```