This is page 61 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/scripts/modules/dependency-manager.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * dependency-manager.js
3 | * Manages task dependencies and relationships
4 | */
5 |
6 | import path from 'path';
7 | import chalk from 'chalk';
8 | import boxen from 'boxen';
9 |
10 | import {
11 | log,
12 | readJSON,
13 | writeJSON,
14 | taskExists,
15 | formatTaskId,
16 | findCycles,
17 | traverseDependencies,
18 | isSilentMode
19 | } from './utils.js';
20 |
21 | import { displayBanner } from './ui.js';
22 |
23 | import generateTaskFiles from './task-manager/generate-task-files.js';
24 |
25 | /**
26 | * Structured error class for dependency operations
27 | */
28 | class DependencyError extends Error {
29 | constructor(code, message, data = {}) {
30 | super(message);
31 | this.name = 'DependencyError';
32 | this.code = code;
33 | this.data = data;
34 | }
35 | }
36 |
37 | /**
38 | * Error codes for dependency operations
39 | */
40 | const DEPENDENCY_ERROR_CODES = {
41 | CANNOT_MOVE_SUBTASK: 'CANNOT_MOVE_SUBTASK',
42 | INVALID_TASK_ID: 'INVALID_TASK_ID',
43 | INVALID_SOURCE_TAG: 'INVALID_SOURCE_TAG',
44 | INVALID_TARGET_TAG: 'INVALID_TARGET_TAG'
45 | };
46 |
47 | /**
48 | * Add a dependency to a task
49 | * @param {string} tasksPath - Path to the tasks.json file
50 | * @param {number|string} taskId - ID of the task to add dependency to
51 | * @param {number|string} dependencyId - ID of the task to add as dependency
52 | * @param {Object} context - Context object containing projectRoot and tag information
53 | * @param {string} [context.projectRoot] - Project root path
54 | * @param {string} [context.tag] - Tag for the task
55 | */
56 | async function addDependency(tasksPath, taskId, dependencyId, context = {}) {
57 | log('info', `Adding dependency ${dependencyId} to task ${taskId}...`);
58 |
59 | const data = readJSON(tasksPath, context.projectRoot, context.tag);
60 | if (!data || !data.tasks) {
61 | log('error', 'No valid tasks found in tasks.json');
62 | process.exit(1);
63 | }
64 |
65 | // Format the task and dependency IDs correctly
66 | const formattedTaskId =
67 | typeof taskId === 'string' && taskId.includes('.')
68 | ? taskId
69 | : parseInt(taskId, 10);
70 |
71 | const formattedDependencyId = formatTaskId(dependencyId);
72 |
73 | // Check if the dependency task or subtask actually exists
74 | if (!taskExists(data.tasks, formattedDependencyId)) {
75 | log(
76 | 'error',
77 | `Dependency target ${formattedDependencyId} does not exist in tasks.json`
78 | );
79 | process.exit(1);
80 | }
81 |
82 | // Find the task to update
83 | let targetTask = null;
84 | let isSubtask = false;
85 |
86 | if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
87 | // Handle dot notation for subtasks (e.g., "1.2")
88 | const [parentId, subtaskId] = formattedTaskId
89 | .split('.')
90 | .map((id) => parseInt(id, 10));
91 | const parentTask = data.tasks.find((t) => t.id === parentId);
92 |
93 | if (!parentTask) {
94 | log('error', `Parent task ${parentId} not found.`);
95 | process.exit(1);
96 | }
97 |
98 | if (!parentTask.subtasks) {
99 | log('error', `Parent task ${parentId} has no subtasks.`);
100 | process.exit(1);
101 | }
102 |
103 | targetTask = parentTask.subtasks.find((s) => s.id === subtaskId);
104 | isSubtask = true;
105 |
106 | if (!targetTask) {
107 | log('error', `Subtask ${formattedTaskId} not found.`);
108 | process.exit(1);
109 | }
110 | } else {
111 | // Regular task (not a subtask)
112 | targetTask = data.tasks.find((t) => t.id === formattedTaskId);
113 |
114 | if (!targetTask) {
115 | log('error', `Task ${formattedTaskId} not found.`);
116 | process.exit(1);
117 | }
118 | }
119 |
120 | // Initialize dependencies array if it doesn't exist
121 | if (!targetTask.dependencies) {
122 | targetTask.dependencies = [];
123 | }
124 |
125 | // Check if dependency already exists
126 | if (
127 | targetTask.dependencies.some((d) => {
128 | // Convert both to strings for comparison to handle both numeric and string IDs
129 | return String(d) === String(formattedDependencyId);
130 | })
131 | ) {
132 | log(
133 | 'warn',
134 | `Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.`
135 | );
136 | return;
137 | }
138 |
139 | // Check if the task is trying to depend on itself - compare full IDs (including subtask parts)
140 | if (String(formattedTaskId) === String(formattedDependencyId)) {
141 | log('error', `Task ${formattedTaskId} cannot depend on itself.`);
142 | process.exit(1);
143 | }
144 |
145 | // For subtasks of the same parent, we need to make sure we're not treating it as a self-dependency
146 | // Check if we're dealing with subtasks with the same parent task
147 | let isSelfDependency = false;
148 |
149 | if (
150 | typeof formattedTaskId === 'string' &&
151 | typeof formattedDependencyId === 'string' &&
152 | formattedTaskId.includes('.') &&
153 | formattedDependencyId.includes('.')
154 | ) {
155 | const [taskParentId] = formattedTaskId.split('.');
156 | const [depParentId] = formattedDependencyId.split('.');
157 |
158 | // Only treat it as a self-dependency if both the parent ID and subtask ID are identical
159 | isSelfDependency = formattedTaskId === formattedDependencyId;
160 |
161 | // Log for debugging
162 | log(
163 | 'debug',
164 | `Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}`
165 | );
166 | log(
167 | 'debug',
168 | `Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}`
169 | );
170 | }
171 |
172 | if (isSelfDependency) {
173 | log('error', `Subtask ${formattedTaskId} cannot depend on itself.`);
174 | process.exit(1);
175 | }
176 |
177 | // Check for circular dependencies
178 | const dependencyChain = [formattedTaskId];
179 | if (
180 | !isCircularDependency(data.tasks, formattedDependencyId, dependencyChain)
181 | ) {
182 | // Add the dependency
183 | targetTask.dependencies.push(formattedDependencyId);
184 |
185 | // Sort dependencies numerically or by parent task ID first, then subtask ID
186 | targetTask.dependencies.sort((a, b) => {
187 | if (typeof a === 'number' && typeof b === 'number') {
188 | return a - b;
189 | } else if (typeof a === 'string' && typeof b === 'string') {
190 | const [aParent, aChild] = a.split('.').map(Number);
191 | const [bParent, bChild] = b.split('.').map(Number);
192 | return aParent !== bParent ? aParent - bParent : aChild - bChild;
193 | } else if (typeof a === 'number') {
194 | return -1; // Numbers come before strings
195 | } else {
196 | return 1; // Strings come after numbers
197 | }
198 | });
199 |
200 | // Save changes
201 | writeJSON(tasksPath, data, context.projectRoot, context.tag);
202 | log(
203 | 'success',
204 | `Added dependency ${formattedDependencyId} to task ${formattedTaskId}`
205 | );
206 |
207 | // Display a more visually appealing success message
208 | if (!isSilentMode()) {
209 | console.log(
210 | boxen(
211 | chalk.green(`Successfully added dependency:\n\n`) +
212 | `Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`,
213 | {
214 | padding: 1,
215 | borderColor: 'green',
216 | borderStyle: 'round',
217 | margin: { top: 1 }
218 | }
219 | )
220 | );
221 | }
222 |
223 | // Generate updated task files
224 | // await generateTaskFiles(tasksPath, path.dirname(tasksPath));
225 |
226 | log('info', 'Task files regenerated with updated dependencies.');
227 | } else {
228 | log(
229 | 'error',
230 | `Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`
231 | );
232 | process.exit(1);
233 | }
234 | }
235 |
236 | /**
237 | * Remove a dependency from a task
238 | * @param {string} tasksPath - Path to the tasks.json file
239 | * @param {number|string} taskId - ID of the task to remove dependency from
240 | * @param {number|string} dependencyId - ID of the task to remove as dependency
241 | * @param {Object} context - Context object containing projectRoot and tag information
242 | * @param {string} [context.projectRoot] - Project root path
243 | * @param {string} [context.tag] - Tag for the task
244 | */
245 | async function removeDependency(tasksPath, taskId, dependencyId, context = {}) {
246 | log('info', `Removing dependency ${dependencyId} from task ${taskId}...`);
247 |
248 | // Read tasks file
249 | const data = readJSON(tasksPath, context.projectRoot, context.tag);
250 | if (!data || !data.tasks) {
251 | log('error', 'No valid tasks found.');
252 | process.exit(1);
253 | }
254 |
255 | // Format the task and dependency IDs correctly
256 | const formattedTaskId =
257 | typeof taskId === 'string' && taskId.includes('.')
258 | ? taskId
259 | : parseInt(taskId, 10);
260 |
261 | const formattedDependencyId = formatTaskId(dependencyId);
262 |
263 | // Find the task to update
264 | let targetTask = null;
265 | let isSubtask = false;
266 |
267 | if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
268 | // Handle dot notation for subtasks (e.g., "1.2")
269 | const [parentId, subtaskId] = formattedTaskId
270 | .split('.')
271 | .map((id) => parseInt(id, 10));
272 | const parentTask = data.tasks.find((t) => t.id === parentId);
273 |
274 | if (!parentTask) {
275 | log('error', `Parent task ${parentId} not found.`);
276 | process.exit(1);
277 | }
278 |
279 | if (!parentTask.subtasks) {
280 | log('error', `Parent task ${parentId} has no subtasks.`);
281 | process.exit(1);
282 | }
283 |
284 | targetTask = parentTask.subtasks.find((s) => s.id === subtaskId);
285 | isSubtask = true;
286 |
287 | if (!targetTask) {
288 | log('error', `Subtask ${formattedTaskId} not found.`);
289 | process.exit(1);
290 | }
291 | } else {
292 | // Regular task (not a subtask)
293 | targetTask = data.tasks.find((t) => t.id === formattedTaskId);
294 |
295 | if (!targetTask) {
296 | log('error', `Task ${formattedTaskId} not found.`);
297 | process.exit(1);
298 | }
299 | }
300 |
301 | // Check if the task has any dependencies
302 | if (!targetTask.dependencies || targetTask.dependencies.length === 0) {
303 | log(
304 | 'info',
305 | `Task ${formattedTaskId} has no dependencies, nothing to remove.`
306 | );
307 | return;
308 | }
309 |
310 | // Normalize the dependency ID for comparison to handle different formats
311 | const normalizedDependencyId = String(formattedDependencyId);
312 |
313 | // Check if the dependency exists by comparing string representations
314 | const dependencyIndex = targetTask.dependencies.findIndex((dep) => {
315 | // Direct string comparison (handles both numeric IDs and dot notation)
316 | const depStr = String(dep);
317 | if (depStr === normalizedDependencyId) {
318 | return true;
319 | }
320 |
321 | // For subtasks: handle numeric dependencies that might be references to other subtasks
322 | // in the same parent (e.g., subtask 1.2 depending on subtask 1.1 stored as just "1")
323 | if (typeof dep === 'number' && dep < 100 && isSubtask) {
324 | const [parentId] = formattedTaskId.split('.');
325 | const fullSubtaskRef = `${parentId}.${dep}`;
326 | if (fullSubtaskRef === normalizedDependencyId) {
327 | return true;
328 | }
329 | }
330 |
331 | return false;
332 | });
333 |
334 | if (dependencyIndex === -1) {
335 | log(
336 | 'info',
337 | `Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`
338 | );
339 | return;
340 | }
341 |
342 | // Remove the dependency
343 | targetTask.dependencies.splice(dependencyIndex, 1);
344 |
345 | // Save the updated tasks
346 | writeJSON(tasksPath, data, context.projectRoot, context.tag);
347 |
348 | // Success message
349 | log(
350 | 'success',
351 | `Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}`
352 | );
353 |
354 | if (!isSilentMode()) {
355 | // Display a more visually appealing success message
356 | console.log(
357 | boxen(
358 | chalk.green(`Successfully removed dependency:\n\n`) +
359 | `Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`,
360 | {
361 | padding: 1,
362 | borderColor: 'green',
363 | borderStyle: 'round',
364 | margin: { top: 1 }
365 | }
366 | )
367 | );
368 | }
369 |
370 | // Regenerate task files
371 | // await generateTaskFiles(tasksPath, path.dirname(tasksPath));
372 | }
373 |
374 | /**
375 | * Check if adding a dependency would create a circular dependency
376 | * @param {Array} tasks - Array of all tasks
377 | * @param {number|string} taskId - ID of task to check
378 | * @param {Array} chain - Chain of dependencies to check
379 | * @returns {boolean} True if circular dependency would be created
380 | */
381 | function isCircularDependency(tasks, taskId, chain = []) {
382 | // Convert taskId to string for comparison
383 | const taskIdStr = String(taskId);
384 |
385 | // If we've seen this task before in the chain, we have a circular dependency
386 | if (chain.some((id) => String(id) === taskIdStr)) {
387 | return true;
388 | }
389 |
390 | // Find the task or subtask
391 | let task = null;
392 | let parentIdForSubtask = null;
393 |
394 | // Check if this is a subtask reference (e.g., "1.2")
395 | if (taskIdStr.includes('.')) {
396 | const [parentId, subtaskId] = taskIdStr.split('.').map(Number);
397 | const parentTask = tasks.find((t) => t.id === parentId);
398 | parentIdForSubtask = parentId; // Store parent ID if it's a subtask
399 |
400 | if (parentTask && parentTask.subtasks) {
401 | task = parentTask.subtasks.find((st) => st.id === subtaskId);
402 | }
403 | } else {
404 | // Regular task - handle both string and numeric task IDs
405 | const taskIdNum = parseInt(taskIdStr, 10);
406 | task = tasks.find((t) => t.id === taskIdNum || String(t.id) === taskIdStr);
407 | }
408 |
409 | if (!task) {
410 | return false; // Task doesn't exist, can't create circular dependency
411 | }
412 |
413 | // No dependencies, can't create circular dependency
414 | if (!task.dependencies || task.dependencies.length === 0) {
415 | return false;
416 | }
417 |
418 | // Check each dependency recursively
419 | const newChain = [...chain, taskIdStr]; // Use taskIdStr for consistency
420 | return task.dependencies.some((depId) => {
421 | let normalizedDepId = String(depId);
422 | // Normalize relative subtask dependencies
423 | if (typeof depId === 'number' && parentIdForSubtask !== null) {
424 | // If the current task is a subtask AND the dependency is a number,
425 | // assume it refers to a sibling subtask.
426 | normalizedDepId = `${parentIdForSubtask}.${depId}`;
427 | }
428 | // Pass the normalized ID to the recursive call
429 | return isCircularDependency(tasks, normalizedDepId, newChain);
430 | });
431 | }
432 |
433 | /**
434 | * Validate task dependencies
435 | * @param {Array} tasks - Array of all tasks
436 | * @returns {Object} Validation result with valid flag and issues array
437 | */
438 | function validateTaskDependencies(tasks) {
439 | const issues = [];
440 |
441 | // Check each task's dependencies
442 | tasks.forEach((task) => {
443 | if (!task.dependencies) {
444 | return; // No dependencies to validate
445 | }
446 |
447 | task.dependencies.forEach((depId) => {
448 | // Check for self-dependencies
449 | if (String(depId) === String(task.id)) {
450 | issues.push({
451 | type: 'self',
452 | taskId: task.id,
453 | message: `Task ${task.id} depends on itself`
454 | });
455 | return;
456 | }
457 |
458 | // Check if dependency exists
459 | if (!taskExists(tasks, depId)) {
460 | issues.push({
461 | type: 'missing',
462 | taskId: task.id,
463 | dependencyId: depId,
464 | message: `Task ${task.id} depends on non-existent task ${depId}`
465 | });
466 | }
467 | });
468 |
469 | // Check for circular dependencies
470 | if (isCircularDependency(tasks, task.id)) {
471 | issues.push({
472 | type: 'circular',
473 | taskId: task.id,
474 | message: `Task ${task.id} is part of a circular dependency chain`
475 | });
476 | }
477 |
478 | // Check subtask dependencies if they exist
479 | if (task.subtasks && task.subtasks.length > 0) {
480 | task.subtasks.forEach((subtask) => {
481 | if (!subtask.dependencies) {
482 | return; // No dependencies to validate
483 | }
484 |
485 | // Create a full subtask ID for reference
486 | const fullSubtaskId = `${task.id}.${subtask.id}`;
487 |
488 | subtask.dependencies.forEach((depId) => {
489 | // Check for self-dependencies in subtasks
490 | if (
491 | String(depId) === String(fullSubtaskId) ||
492 | (typeof depId === 'number' && depId === subtask.id)
493 | ) {
494 | issues.push({
495 | type: 'self',
496 | taskId: fullSubtaskId,
497 | message: `Subtask ${fullSubtaskId} depends on itself`
498 | });
499 | return;
500 | }
501 |
502 | // Check if dependency exists
503 | if (!taskExists(tasks, depId)) {
504 | issues.push({
505 | type: 'missing',
506 | taskId: fullSubtaskId,
507 | dependencyId: depId,
508 | message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`
509 | });
510 | }
511 | });
512 |
513 | // Check for circular dependencies in subtasks
514 | if (isCircularDependency(tasks, fullSubtaskId)) {
515 | issues.push({
516 | type: 'circular',
517 | taskId: fullSubtaskId,
518 | message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`
519 | });
520 | }
521 | });
522 | }
523 | });
524 |
525 | return {
526 | valid: issues.length === 0,
527 | issues
528 | };
529 | }
530 |
531 | /**
532 | * Remove duplicate dependencies from tasks
533 | * @param {Object} tasksData - Tasks data object with tasks array
534 | * @returns {Object} Updated tasks data with duplicates removed
535 | */
536 | function removeDuplicateDependencies(tasksData) {
537 | const tasks = tasksData.tasks.map((task) => {
538 | if (!task.dependencies) {
539 | return task;
540 | }
541 |
542 | // Convert to Set and back to array to remove duplicates
543 | const uniqueDeps = [...new Set(task.dependencies)];
544 | return {
545 | ...task,
546 | dependencies: uniqueDeps
547 | };
548 | });
549 |
550 | return {
551 | ...tasksData,
552 | tasks
553 | };
554 | }
555 |
556 | /**
557 | * Clean up invalid subtask dependencies
558 | * @param {Object} tasksData - Tasks data object with tasks array
559 | * @returns {Object} Updated tasks data with invalid subtask dependencies removed
560 | */
561 | function cleanupSubtaskDependencies(tasksData) {
562 | const tasks = tasksData.tasks.map((task) => {
563 | // Handle task's own dependencies
564 | if (task.dependencies) {
565 | task.dependencies = task.dependencies.filter((depId) => {
566 | // Keep only dependencies that exist
567 | return taskExists(tasksData.tasks, depId);
568 | });
569 | }
570 |
571 | // Handle subtask dependencies
572 | if (task.subtasks) {
573 | task.subtasks = task.subtasks.map((subtask) => {
574 | if (!subtask.dependencies) {
575 | return subtask;
576 | }
577 |
578 | // Filter out dependencies to non-existent subtasks
579 | subtask.dependencies = subtask.dependencies.filter((depId) => {
580 | return taskExists(tasksData.tasks, depId);
581 | });
582 |
583 | return subtask;
584 | });
585 | }
586 |
587 | return task;
588 | });
589 |
590 | return {
591 | ...tasksData,
592 | tasks
593 | };
594 | }
595 |
596 | /**
597 | * Validate dependencies in task files
598 | * @param {string} tasksPath - Path to tasks.json
599 | * @param {Object} options - Options object, including context
600 | */
601 | async function validateDependenciesCommand(tasksPath, options = {}) {
602 | const { context = {} } = options;
603 | log('info', 'Checking for invalid dependencies in task files...');
604 |
605 | // Read tasks data
606 | const data = readJSON(tasksPath, context.projectRoot, context.tag);
607 | if (!data || !data.tasks) {
608 | log('error', 'No valid tasks found in tasks.json');
609 | process.exit(1);
610 | }
611 |
612 | // Count of tasks and subtasks for reporting
613 | const taskCount = data.tasks.length;
614 | let subtaskCount = 0;
615 | data.tasks.forEach((task) => {
616 | if (task.subtasks && Array.isArray(task.subtasks)) {
617 | subtaskCount += task.subtasks.length;
618 | }
619 | });
620 |
621 | log(
622 | 'info',
623 | `Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...`
624 | );
625 |
626 | try {
627 | // Directly call the validation function
628 | const validationResult = validateTaskDependencies(data.tasks);
629 |
630 | if (!validationResult.valid) {
631 | log(
632 | 'error',
633 | `Dependency validation failed. Found ${validationResult.issues.length} issue(s):`
634 | );
635 | validationResult.issues.forEach((issue) => {
636 | let errorMsg = ` [${issue.type.toUpperCase()}] Task ${issue.taskId}: ${issue.message}`;
637 | if (issue.dependencyId) {
638 | errorMsg += ` (Dependency: ${issue.dependencyId})`;
639 | }
640 | log('error', errorMsg); // Log each issue as an error
641 | });
642 |
643 | // Optionally exit if validation fails, depending on desired behavior
644 | // process.exit(1); // Uncomment if validation failure should stop the process
645 |
646 | // Display summary box even on failure, showing issues found
647 | if (!isSilentMode()) {
648 | console.log(
649 | boxen(
650 | chalk.red(`Dependency Validation FAILED\n\n`) +
651 | `${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
652 | `${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
653 | `${chalk.red('Issues found:')} ${validationResult.issues.length}`, // Display count from result
654 | {
655 | padding: 1,
656 | borderColor: 'red',
657 | borderStyle: 'round',
658 | margin: { top: 1, bottom: 1 }
659 | }
660 | )
661 | );
662 | }
663 | } else {
664 | log(
665 | 'success',
666 | 'No invalid dependencies found - all dependencies are valid'
667 | );
668 |
669 | // Show validation summary - only if not in silent mode
670 | if (!isSilentMode()) {
671 | console.log(
672 | boxen(
673 | chalk.green(`All Dependencies Are Valid\n\n`) +
674 | `${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
675 | `${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
676 | `${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
677 | {
678 | padding: 1,
679 | borderColor: 'green',
680 | borderStyle: 'round',
681 | margin: { top: 1, bottom: 1 }
682 | }
683 | )
684 | );
685 | }
686 | }
687 | } catch (error) {
688 | log('error', 'Error validating dependencies:', error);
689 | process.exit(1);
690 | }
691 | }
692 |
693 | /**
694 | * Helper function to count all dependencies across tasks and subtasks
695 | * @param {Array} tasks - All tasks
696 | * @returns {number} - Total number of dependencies
697 | */
698 | function countAllDependencies(tasks) {
699 | let count = 0;
700 |
701 | tasks.forEach((task) => {
702 | // Count main task dependencies
703 | if (task.dependencies && Array.isArray(task.dependencies)) {
704 | count += task.dependencies.length;
705 | }
706 |
707 | // Count subtask dependencies
708 | if (task.subtasks && Array.isArray(task.subtasks)) {
709 | task.subtasks.forEach((subtask) => {
710 | if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
711 | count += subtask.dependencies.length;
712 | }
713 | });
714 | }
715 | });
716 |
717 | return count;
718 | }
719 |
720 | /**
721 | * Fixes invalid dependencies in tasks.json
722 | * @param {string} tasksPath - Path to tasks.json
723 | * @param {Object} options - Options object, including context
724 | */
725 | async function fixDependenciesCommand(tasksPath, options = {}) {
726 | const { context = {} } = options;
727 | log('info', 'Checking for and fixing invalid dependencies in tasks.json...');
728 |
729 | try {
730 | // Read tasks data
731 | const data = readJSON(tasksPath, context.projectRoot, context.tag);
732 | if (!data || !data.tasks) {
733 | log('error', 'No valid tasks found in tasks.json');
734 | process.exit(1);
735 | }
736 |
737 | // Create a deep copy of the original data for comparison
738 | const originalData = JSON.parse(JSON.stringify(data));
739 |
740 | // Track fixes for reporting
741 | const stats = {
742 | nonExistentDependenciesRemoved: 0,
743 | selfDependenciesRemoved: 0,
744 | duplicateDependenciesRemoved: 0,
745 | circularDependenciesFixed: 0,
746 | tasksFixed: 0,
747 | subtasksFixed: 0
748 | };
749 |
750 | // First phase: Remove duplicate dependencies in tasks
751 | data.tasks.forEach((task) => {
752 | if (task.dependencies && Array.isArray(task.dependencies)) {
753 | const uniqueDeps = new Set();
754 | const originalLength = task.dependencies.length;
755 | task.dependencies = task.dependencies.filter((depId) => {
756 | const depIdStr = String(depId);
757 | if (uniqueDeps.has(depIdStr)) {
758 | log(
759 | 'info',
760 | `Removing duplicate dependency from task ${task.id}: ${depId}`
761 | );
762 | stats.duplicateDependenciesRemoved++;
763 | return false;
764 | }
765 | uniqueDeps.add(depIdStr);
766 | return true;
767 | });
768 | if (task.dependencies.length < originalLength) {
769 | stats.tasksFixed++;
770 | }
771 | }
772 |
773 | // Check for duplicates in subtasks
774 | if (task.subtasks && Array.isArray(task.subtasks)) {
775 | task.subtasks.forEach((subtask) => {
776 | if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
777 | const uniqueDeps = new Set();
778 | const originalLength = subtask.dependencies.length;
779 | subtask.dependencies = subtask.dependencies.filter((depId) => {
780 | let depIdStr = String(depId);
781 | if (typeof depId === 'number' && depId < 100) {
782 | depIdStr = `${task.id}.${depId}`;
783 | }
784 | if (uniqueDeps.has(depIdStr)) {
785 | log(
786 | 'info',
787 | `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`
788 | );
789 | stats.duplicateDependenciesRemoved++;
790 | return false;
791 | }
792 | uniqueDeps.add(depIdStr);
793 | return true;
794 | });
795 | if (subtask.dependencies.length < originalLength) {
796 | stats.subtasksFixed++;
797 | }
798 | }
799 | });
800 | }
801 | });
802 |
803 | // Create validity maps for tasks and subtasks
804 | const validTaskIds = new Set(data.tasks.map((t) => t.id));
805 | const validSubtaskIds = new Set();
806 | data.tasks.forEach((task) => {
807 | if (task.subtasks && Array.isArray(task.subtasks)) {
808 | task.subtasks.forEach((subtask) => {
809 | validSubtaskIds.add(`${task.id}.${subtask.id}`);
810 | });
811 | }
812 | });
813 |
814 | // Second phase: Remove invalid task dependencies (non-existent tasks)
815 | data.tasks.forEach((task) => {
816 | if (task.dependencies && Array.isArray(task.dependencies)) {
817 | const originalLength = task.dependencies.length;
818 | task.dependencies = task.dependencies.filter((depId) => {
819 | const isSubtask = typeof depId === 'string' && depId.includes('.');
820 |
821 | if (isSubtask) {
822 | // Check if the subtask exists
823 | if (!validSubtaskIds.has(depId)) {
824 | log(
825 | 'info',
826 | `Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`
827 | );
828 | stats.nonExistentDependenciesRemoved++;
829 | return false;
830 | }
831 | return true;
832 | } else {
833 | // Check if the task exists
834 | const numericId =
835 | typeof depId === 'string' ? parseInt(depId, 10) : depId;
836 | if (!validTaskIds.has(numericId)) {
837 | log(
838 | 'info',
839 | `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`
840 | );
841 | stats.nonExistentDependenciesRemoved++;
842 | return false;
843 | }
844 | return true;
845 | }
846 | });
847 |
848 | if (task.dependencies.length < originalLength) {
849 | stats.tasksFixed++;
850 | }
851 | }
852 |
853 | // Check subtask dependencies for invalid references
854 | if (task.subtasks && Array.isArray(task.subtasks)) {
855 | task.subtasks.forEach((subtask) => {
856 | if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
857 | const originalLength = subtask.dependencies.length;
858 | const subtaskId = `${task.id}.${subtask.id}`;
859 |
860 | // First check for self-dependencies
861 | const hasSelfDependency = subtask.dependencies.some((depId) => {
862 | if (typeof depId === 'string' && depId.includes('.')) {
863 | return depId === subtaskId;
864 | } else if (typeof depId === 'number' && depId < 100) {
865 | return depId === subtask.id;
866 | }
867 | return false;
868 | });
869 |
870 | if (hasSelfDependency) {
871 | subtask.dependencies = subtask.dependencies.filter((depId) => {
872 | const normalizedDepId =
873 | typeof depId === 'number' && depId < 100
874 | ? `${task.id}.${depId}`
875 | : String(depId);
876 |
877 | if (normalizedDepId === subtaskId) {
878 | log(
879 | 'info',
880 | `Removing self-dependency from subtask ${subtaskId}`
881 | );
882 | stats.selfDependenciesRemoved++;
883 | return false;
884 | }
885 | return true;
886 | });
887 | }
888 |
889 | // Then check for non-existent dependencies
890 | subtask.dependencies = subtask.dependencies.filter((depId) => {
891 | if (typeof depId === 'string' && depId.includes('.')) {
892 | if (!validSubtaskIds.has(depId)) {
893 | log(
894 | 'info',
895 | `Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)`
896 | );
897 | stats.nonExistentDependenciesRemoved++;
898 | return false;
899 | }
900 | return true;
901 | }
902 |
903 | // Handle numeric dependencies
904 | const numericId =
905 | typeof depId === 'number' ? depId : parseInt(depId, 10);
906 |
907 | // Small numbers likely refer to subtasks in the same task
908 | if (numericId < 100) {
909 | const fullSubtaskId = `${task.id}.${numericId}`;
910 |
911 | if (!validSubtaskIds.has(fullSubtaskId)) {
912 | log(
913 | 'info',
914 | `Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}`
915 | );
916 | stats.nonExistentDependenciesRemoved++;
917 | return false;
918 | }
919 |
920 | return true;
921 | }
922 |
923 | // Otherwise it's a task reference
924 | if (!validTaskIds.has(numericId)) {
925 | log(
926 | 'info',
927 | `Removing invalid task dependency from subtask ${subtaskId}: ${numericId}`
928 | );
929 | stats.nonExistentDependenciesRemoved++;
930 | return false;
931 | }
932 |
933 | return true;
934 | });
935 |
936 | if (subtask.dependencies.length < originalLength) {
937 | stats.subtasksFixed++;
938 | }
939 | }
940 | });
941 | }
942 | });
943 |
944 | // Third phase: Check for circular dependencies
945 | log('info', 'Checking for circular dependencies...');
946 |
947 | // Build the dependency map for subtasks
948 | const subtaskDependencyMap = new Map();
949 | data.tasks.forEach((task) => {
950 | if (task.subtasks && Array.isArray(task.subtasks)) {
951 | task.subtasks.forEach((subtask) => {
952 | const subtaskId = `${task.id}.${subtask.id}`;
953 |
954 | if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
955 | const normalizedDeps = subtask.dependencies.map((depId) => {
956 | if (typeof depId === 'string' && depId.includes('.')) {
957 | return depId;
958 | } else if (typeof depId === 'number' && depId < 100) {
959 | return `${task.id}.${depId}`;
960 | }
961 | return String(depId);
962 | });
963 | subtaskDependencyMap.set(subtaskId, normalizedDeps);
964 | } else {
965 | subtaskDependencyMap.set(subtaskId, []);
966 | }
967 | });
968 | }
969 | });
970 |
971 | // Check for and fix circular dependencies
972 | for (const [subtaskId, dependencies] of subtaskDependencyMap.entries()) {
973 | const visited = new Set();
974 | const recursionStack = new Set();
975 |
976 | // Detect cycles
977 | const cycleEdges = findCycles(
978 | subtaskId,
979 | subtaskDependencyMap,
980 | visited,
981 | recursionStack
982 | );
983 |
984 | if (cycleEdges.length > 0) {
985 | const [taskId, subtaskNum] = subtaskId
986 | .split('.')
987 | .map((part) => Number(part));
988 | const task = data.tasks.find((t) => t.id === taskId);
989 |
990 | if (task && task.subtasks) {
991 | const subtask = task.subtasks.find((st) => st.id === subtaskNum);
992 |
993 | if (subtask && subtask.dependencies) {
994 | const originalLength = subtask.dependencies.length;
995 |
996 | const edgesToRemove = cycleEdges.map((edge) => {
997 | if (edge.includes('.')) {
998 | const [depTaskId, depSubtaskId] = edge
999 | .split('.')
1000 | .map((part) => Number(part));
1001 |
1002 | if (depTaskId === taskId) {
1003 | return depSubtaskId;
1004 | }
1005 |
1006 | return edge;
1007 | }
1008 |
1009 | return Number(edge);
1010 | });
1011 |
1012 | subtask.dependencies = subtask.dependencies.filter((depId) => {
1013 | const normalizedDepId =
1014 | typeof depId === 'number' && depId < 100
1015 | ? `${taskId}.${depId}`
1016 | : String(depId);
1017 |
1018 | if (
1019 | edgesToRemove.includes(depId) ||
1020 | edgesToRemove.includes(normalizedDepId)
1021 | ) {
1022 | log(
1023 | 'info',
1024 | `Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}`
1025 | );
1026 | stats.circularDependenciesFixed++;
1027 | return false;
1028 | }
1029 | return true;
1030 | });
1031 |
1032 | if (subtask.dependencies.length < originalLength) {
1033 | stats.subtasksFixed++;
1034 | }
1035 | }
1036 | }
1037 | }
1038 | }
1039 |
1040 | // Check if any changes were made by comparing with original data
1041 | const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData);
1042 |
1043 | if (dataChanged) {
1044 | // Save the changes
1045 | writeJSON(tasksPath, data, context.projectRoot, context.tag);
1046 | log('success', 'Fixed dependency issues in tasks.json');
1047 |
1048 | // Regenerate task files
1049 | log('info', 'Regenerating task files to reflect dependency changes...');
1050 | // await generateTaskFiles(tasksPath, path.dirname(tasksPath));
1051 | } else {
1052 | log('info', 'No changes needed to fix dependencies');
1053 | }
1054 |
1055 | // Show detailed statistics report
1056 | const totalFixedAll =
1057 | stats.nonExistentDependenciesRemoved +
1058 | stats.selfDependenciesRemoved +
1059 | stats.duplicateDependenciesRemoved +
1060 | stats.circularDependenciesFixed;
1061 |
1062 | if (!isSilentMode()) {
1063 | if (totalFixedAll > 0) {
1064 | log('success', `Fixed ${totalFixedAll} dependency issues in total!`);
1065 |
1066 | console.log(
1067 | boxen(
1068 | chalk.green(`Dependency Fixes Summary:\n\n`) +
1069 | `${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` +
1070 | `${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` +
1071 | `${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` +
1072 | `${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` +
1073 | `${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` +
1074 | `${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`,
1075 | {
1076 | padding: 1,
1077 | borderColor: 'green',
1078 | borderStyle: 'round',
1079 | margin: { top: 1, bottom: 1 }
1080 | }
1081 | )
1082 | );
1083 | } else {
1084 | log(
1085 | 'success',
1086 | 'No dependency issues found - all dependencies are valid'
1087 | );
1088 |
1089 | console.log(
1090 | boxen(
1091 | chalk.green(`All Dependencies Are Valid\n\n`) +
1092 | `${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` +
1093 | `${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
1094 | {
1095 | padding: 1,
1096 | borderColor: 'green',
1097 | borderStyle: 'round',
1098 | margin: { top: 1, bottom: 1 }
1099 | }
1100 | )
1101 | );
1102 | }
1103 | }
1104 | } catch (error) {
1105 | log('error', 'Error in fix-dependencies command:', error);
1106 | process.exit(1);
1107 | }
1108 | }
1109 |
1110 | /**
1111 | * Ensure at least one subtask in each task has no dependencies
1112 | * @param {Object} tasksData - The tasks data object with tasks array
1113 | * @returns {boolean} - True if any changes were made
1114 | */
1115 | function ensureAtLeastOneIndependentSubtask(tasksData) {
1116 | if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
1117 | return false;
1118 | }
1119 |
1120 | let changesDetected = false;
1121 |
1122 | tasksData.tasks.forEach((task) => {
1123 | if (
1124 | !task.subtasks ||
1125 | !Array.isArray(task.subtasks) ||
1126 | task.subtasks.length === 0
1127 | ) {
1128 | return;
1129 | }
1130 |
1131 | // Check if any subtask has no dependencies
1132 | const hasIndependentSubtask = task.subtasks.some(
1133 | (st) =>
1134 | !st.dependencies ||
1135 | !Array.isArray(st.dependencies) ||
1136 | st.dependencies.length === 0
1137 | );
1138 |
1139 | if (!hasIndependentSubtask) {
1140 | // Find the first subtask and clear its dependencies
1141 | if (task.subtasks.length > 0) {
1142 | const firstSubtask = task.subtasks[0];
1143 | log(
1144 | 'debug',
1145 | `Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}`
1146 | );
1147 | firstSubtask.dependencies = [];
1148 | changesDetected = true;
1149 | }
1150 | }
1151 | });
1152 |
1153 | return changesDetected;
1154 | }
1155 |
1156 | /**
1157 | * Validate and fix dependencies across all tasks and subtasks
1158 | * This function is designed to be called after any task modification
1159 | * @param {Object} tasksData - The tasks data object with tasks array
1160 | * @param {string} tasksPath - Optional path to save the changes
1161 | * @param {string} projectRoot - Optional project root for tag context
1162 | * @param {string} tag - Optional tag for tag context
1163 | * @returns {boolean} - True if any changes were made
1164 | */
1165 | function validateAndFixDependencies(
1166 | tasksData,
1167 | tasksPath = null,
1168 | projectRoot = null,
1169 | tag = null
1170 | ) {
1171 | if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
1172 | log('error', 'Invalid tasks data');
1173 | return false;
1174 | }
1175 |
1176 | log('debug', 'Validating and fixing dependencies...');
1177 |
1178 | // Create a deep copy for comparison
1179 | const originalData = JSON.parse(JSON.stringify(tasksData));
1180 |
1181 | // 1. Remove duplicate dependencies from tasks and subtasks
1182 | tasksData.tasks = tasksData.tasks.map((task) => {
1183 | // Handle task dependencies
1184 | if (task.dependencies) {
1185 | const uniqueDeps = [...new Set(task.dependencies)];
1186 | task.dependencies = uniqueDeps;
1187 | }
1188 |
1189 | // Handle subtask dependencies
1190 | if (task.subtasks) {
1191 | task.subtasks = task.subtasks.map((subtask) => {
1192 | if (subtask.dependencies) {
1193 | const uniqueDeps = [...new Set(subtask.dependencies)];
1194 | subtask.dependencies = uniqueDeps;
1195 | }
1196 | return subtask;
1197 | });
1198 | }
1199 | return task;
1200 | });
1201 |
1202 | // 2. Remove invalid task dependencies (non-existent tasks)
1203 | tasksData.tasks.forEach((task) => {
1204 | // Clean up task dependencies
1205 | if (task.dependencies) {
1206 | task.dependencies = task.dependencies.filter((depId) => {
1207 | // Remove self-dependencies
1208 | if (String(depId) === String(task.id)) {
1209 | return false;
1210 | }
1211 | // Remove non-existent dependencies
1212 | return taskExists(tasksData.tasks, depId);
1213 | });
1214 | }
1215 |
1216 | // Clean up subtask dependencies
1217 | if (task.subtasks) {
1218 | task.subtasks.forEach((subtask) => {
1219 | if (subtask.dependencies) {
1220 | subtask.dependencies = subtask.dependencies.filter((depId) => {
1221 | // Handle numeric subtask references
1222 | if (typeof depId === 'number' && depId < 100) {
1223 | const fullSubtaskId = `${task.id}.${depId}`;
1224 | return taskExists(tasksData.tasks, fullSubtaskId);
1225 | }
1226 | // Handle full task/subtask references
1227 | return taskExists(tasksData.tasks, depId);
1228 | });
1229 | }
1230 | });
1231 | }
1232 | });
1233 |
1234 | // 3. Ensure at least one subtask has no dependencies in each task
1235 | tasksData.tasks.forEach((task) => {
1236 | if (task.subtasks && task.subtasks.length > 0) {
1237 | const hasIndependentSubtask = task.subtasks.some(
1238 | (st) =>
1239 | !st.dependencies ||
1240 | !Array.isArray(st.dependencies) ||
1241 | st.dependencies.length === 0
1242 | );
1243 |
1244 | if (!hasIndependentSubtask) {
1245 | task.subtasks[0].dependencies = [];
1246 | }
1247 | }
1248 | });
1249 |
1250 | // Check if any changes were made by comparing with original data
1251 | const changesDetected =
1252 | JSON.stringify(tasksData) !== JSON.stringify(originalData);
1253 |
1254 | // Save changes if needed
1255 | if (tasksPath && changesDetected) {
1256 | try {
1257 | writeJSON(tasksPath, tasksData, projectRoot, tag);
1258 | log('debug', 'Saved dependency fixes to tasks.json');
1259 | } catch (error) {
1260 | log('error', 'Failed to save dependency fixes to tasks.json', error);
1261 | }
1262 | }
1263 |
1264 | return changesDetected;
1265 | }
1266 |
1267 | /**
1268 | * Recursively find all dependencies for a set of tasks with depth limiting
1269 | * Recursively find all dependencies for a set of tasks with depth limiting
1270 | *
1271 | * @note This function depends on the traverseDependencies utility from utils.js
1272 | * for the actual dependency traversal logic.
1273 | *
1274 | * @param {Array} sourceTasks - Array of source tasks to find dependencies for
1275 | * @param {Array} allTasks - Array of all available tasks
1276 | * @param {Object} options - Options object
1277 | * @param {number} options.maxDepth - Maximum recursion depth (default: 50)
1278 | * @param {boolean} options.includeSelf - Whether to include self-references (default: false)
1279 | * @returns {Array} Array of all dependency task IDs
1280 | */
1281 | function findAllDependenciesRecursively(sourceTasks, allTasks, options = {}) {
1282 | if (!Array.isArray(sourceTasks)) {
1283 | throw new Error('Source tasks parameter must be an array');
1284 | }
1285 | if (!Array.isArray(allTasks)) {
1286 | throw new Error('All tasks parameter must be an array');
1287 | }
1288 | return traverseDependencies(sourceTasks, allTasks, {
1289 | ...options,
1290 | direction: 'forward',
1291 | logger: { warn: log.warn || console.warn }
1292 | });
1293 | }
1294 |
1295 | /**
1296 | * Find dependency task by ID, handling various ID formats
1297 | * @param {string|number} depId - Dependency ID to find
1298 | * @param {string} taskId - ID of the task that has this dependency
1299 | * @param {Array} allTasks - Array of all tasks to search
1300 | * @returns {Object|null} Found dependency task or null
1301 | */
1302 | /**
1303 | * Find a subtask within a parent task's subtasks array
1304 | * @param {string} parentId - The parent task ID
1305 | * @param {string|number} subtaskId - The subtask ID to find
1306 | * @param {Array} allTasks - Array of all tasks to search in
1307 | * @param {boolean} useStringComparison - Whether to use string comparison for subtaskId
1308 | * @returns {Object|null} The found subtask with full ID or null if not found
1309 | */
1310 | function findSubtaskInParent(
1311 | parentId,
1312 | subtaskId,
1313 | allTasks,
1314 | useStringComparison = false
1315 | ) {
1316 | // Convert parentId to numeric for proper comparison with top-level task IDs
1317 | const numericParentId = parseInt(parentId, 10);
1318 | const parentTask = allTasks.find((t) => t.id === numericParentId);
1319 |
1320 | if (parentTask && parentTask.subtasks && Array.isArray(parentTask.subtasks)) {
1321 | const foundSubtask = parentTask.subtasks.find((subtask) =>
1322 | useStringComparison
1323 | ? String(subtask.id) === String(subtaskId)
1324 | : subtask.id === subtaskId
1325 | );
1326 | if (foundSubtask) {
1327 | // Return a task-like object that represents the subtask with full ID
1328 | return {
1329 | ...foundSubtask,
1330 | id: `${parentId}.${foundSubtask.id}`
1331 | };
1332 | }
1333 | }
1334 |
1335 | return null;
1336 | }
1337 |
1338 | function findDependencyTask(depId, taskId, allTasks) {
1339 | if (!depId) {
1340 | return null;
1341 | }
1342 |
1343 | // Convert depId to string for consistent comparison
1344 | const depIdStr = String(depId);
1345 |
1346 | // Find the dependency task - handle both top-level and subtask IDs
1347 | let depTask = null;
1348 |
1349 | // First try exact match (for top-level tasks)
1350 | depTask = allTasks.find((t) => String(t.id) === depIdStr);
1351 |
1352 | // If not found and it's a subtask reference (contains dot), find the parent task first
1353 | if (!depTask && depIdStr.includes('.')) {
1354 | const [parentId, subtaskId] = depIdStr.split('.');
1355 | depTask = findSubtaskInParent(parentId, subtaskId, allTasks, true);
1356 | }
1357 |
1358 | // If still not found, try numeric comparison for relative subtask references
1359 | if (!depTask && !isNaN(depId)) {
1360 | const numericId = parseInt(depId, 10);
1361 | // For subtasks, this might be a relative reference within the same parent
1362 | if (taskId && typeof taskId === 'string' && taskId.includes('.')) {
1363 | const [parentId] = taskId.split('.');
1364 | depTask = findSubtaskInParent(parentId, numericId, allTasks, false);
1365 | }
1366 | }
1367 |
1368 | return depTask;
1369 | }
1370 |
1371 | /**
1372 | * Check if a task has cross-tag dependencies
1373 | * @param {Object} task - Task to check
1374 | * @param {string} targetTag - Target tag name
1375 | * @param {Array} allTasks - Array of all tasks from all tags
1376 | * @returns {Array} Array of cross-tag dependency conflicts
1377 | */
1378 | function findTaskCrossTagConflicts(task, targetTag, allTasks) {
1379 | const conflicts = [];
1380 |
1381 | // Validate task.dependencies is an array before processing
1382 | if (!Array.isArray(task.dependencies) || task.dependencies.length === 0) {
1383 | return conflicts;
1384 | }
1385 |
1386 | // Filter out null/undefined dependencies and check each valid dependency
1387 | const validDependencies = task.dependencies.filter((depId) => depId != null);
1388 |
1389 | validDependencies.forEach((depId) => {
1390 | const depTask = findDependencyTask(depId, task.id, allTasks);
1391 |
1392 | if (depTask && depTask.tag !== targetTag) {
1393 | conflicts.push({
1394 | taskId: task.id,
1395 | dependencyId: depId,
1396 | dependencyTag: depTask.tag,
1397 | message: `Task ${task.id} depends on ${depId} (in ${depTask.tag})`
1398 | });
1399 | }
1400 | });
1401 |
1402 | return conflicts;
1403 | }
1404 |
1405 | function validateCrossTagMove(task, sourceTag, targetTag, allTasks) {
1406 | // Parameter validation
1407 | if (!task || typeof task !== 'object') {
1408 | throw new Error('Task parameter must be a valid object');
1409 | }
1410 |
1411 | if (!sourceTag || typeof sourceTag !== 'string') {
1412 | throw new Error('Source tag must be a valid string');
1413 | }
1414 |
1415 | if (!targetTag || typeof targetTag !== 'string') {
1416 | throw new Error('Target tag must be a valid string');
1417 | }
1418 |
1419 | if (!Array.isArray(allTasks)) {
1420 | throw new Error('All tasks parameter must be an array');
1421 | }
1422 |
1423 | const conflicts = findTaskCrossTagConflicts(task, targetTag, allTasks);
1424 |
1425 | return {
1426 | canMove: conflicts.length === 0,
1427 | conflicts
1428 | };
1429 | }
1430 |
1431 | /**
1432 | * Find all cross-tag dependencies for a set of tasks
1433 | * @param {Array} sourceTasks - Array of tasks to check
1434 | * @param {string} sourceTag - Source tag name
1435 | * @param {string} targetTag - Target tag name
1436 | * @param {Array} allTasks - Array of all tasks from all tags
1437 | * @returns {Array} Array of cross-tag dependency conflicts
1438 | */
1439 | function findCrossTagDependencies(sourceTasks, sourceTag, targetTag, allTasks) {
1440 | // Parameter validation
1441 | if (!Array.isArray(sourceTasks)) {
1442 | throw new Error('Source tasks parameter must be an array');
1443 | }
1444 |
1445 | if (!sourceTag || typeof sourceTag !== 'string') {
1446 | throw new Error('Source tag must be a valid string');
1447 | }
1448 |
1449 | if (!targetTag || typeof targetTag !== 'string') {
1450 | throw new Error('Target tag must be a valid string');
1451 | }
1452 |
1453 | if (!Array.isArray(allTasks)) {
1454 | throw new Error('All tasks parameter must be an array');
1455 | }
1456 |
1457 | const conflicts = [];
1458 |
1459 | sourceTasks.forEach((task) => {
1460 | // Validate task object and dependencies array
1461 | if (
1462 | !task ||
1463 | typeof task !== 'object' ||
1464 | !Array.isArray(task.dependencies) ||
1465 | task.dependencies.length === 0
1466 | ) {
1467 | return;
1468 | }
1469 |
1470 | // Use the shared helper function to find conflicts for this task
1471 | const taskConflicts = findTaskCrossTagConflicts(task, targetTag, allTasks);
1472 | conflicts.push(...taskConflicts);
1473 | });
1474 |
1475 | return conflicts;
1476 | }
1477 |
1478 | /**
1479 | * Helper function to find all tasks that depend on a given task (reverse dependencies)
1480 | * @param {string|number} taskId - The task ID to find dependencies for
1481 | * @param {Array} allTasks - Array of all tasks to search
1482 | * @param {Set} dependentTaskIds - Set to add found dependencies to
1483 | */
1484 | function findTasksThatDependOn(taskId, allTasks, dependentTaskIds) {
1485 | // Find the task object for the given ID
1486 | const sourceTask = allTasks.find((t) => t.id === taskId);
1487 | if (!sourceTask) {
1488 | return;
1489 | }
1490 |
1491 | // Use the shared utility for reverse dependency traversal
1492 | const reverseDeps = traverseDependencies([sourceTask], allTasks, {
1493 | direction: 'reverse',
1494 | includeSelf: false,
1495 | logger: { warn: log.warn || console.warn }
1496 | });
1497 |
1498 | // Add all found reverse dependencies to the dependentTaskIds set
1499 | reverseDeps.forEach((depId) => dependentTaskIds.add(depId));
1500 | }
1501 |
1502 | /**
1503 | * Helper function to check if a task depends on a source task
1504 | * @param {Object} task - Task to check for dependencies
1505 | * @param {Object} sourceTask - Source task to check dependency against
1506 | * @returns {boolean} True if task depends on source task
1507 | */
1508 | function taskDependsOnSource(task, sourceTask) {
1509 | if (!task || !Array.isArray(task.dependencies)) {
1510 | return false;
1511 | }
1512 |
1513 | const sourceTaskIdStr = String(sourceTask.id);
1514 |
1515 | return task.dependencies.some((depId) => {
1516 | if (!depId) return false;
1517 |
1518 | const depIdStr = String(depId);
1519 |
1520 | // Exact match
1521 | if (depIdStr === sourceTaskIdStr) {
1522 | return true;
1523 | }
1524 |
1525 | // Handle subtask references
1526 | if (
1527 | sourceTaskIdStr &&
1528 | typeof sourceTaskIdStr === 'string' &&
1529 | sourceTaskIdStr.includes('.')
1530 | ) {
1531 | // If source is a subtask, check if dependency references the parent
1532 | const [parentId] = sourceTaskIdStr.split('.');
1533 | if (depIdStr === parentId) {
1534 | return true;
1535 | }
1536 | }
1537 |
1538 | // Handle relative subtask references
1539 | if (
1540 | depIdStr &&
1541 | typeof depIdStr === 'string' &&
1542 | depIdStr.includes('.') &&
1543 | sourceTaskIdStr &&
1544 | typeof sourceTaskIdStr === 'string' &&
1545 | sourceTaskIdStr.includes('.')
1546 | ) {
1547 | const [depParentId] = depIdStr.split('.');
1548 | const [sourceParentId] = sourceTaskIdStr.split('.');
1549 | if (depParentId === sourceParentId) {
1550 | // Both are subtasks of the same parent, check if they reference each other
1551 | const depSubtaskNum = parseInt(depIdStr.split('.')[1], 10);
1552 | const sourceSubtaskNum = parseInt(sourceTaskIdStr.split('.')[1], 10);
1553 | if (depSubtaskNum === sourceSubtaskNum) {
1554 | return true;
1555 | }
1556 | }
1557 | }
1558 |
1559 | return false;
1560 | });
1561 | }
1562 |
1563 | /**
1564 | * Helper function to check if any subtasks of a task depend on source tasks
1565 | * @param {Object} task - Task to check subtasks of
1566 | * @param {Array} sourceTasks - Array of source tasks to check dependencies against
1567 | * @returns {boolean} True if any subtasks depend on source tasks
1568 | */
1569 | function subtasksDependOnSource(task, sourceTasks) {
1570 | if (!task.subtasks || !Array.isArray(task.subtasks)) {
1571 | return false;
1572 | }
1573 |
1574 | return task.subtasks.some((subtask) => {
1575 | // Check if this subtask depends on any source task
1576 | const subtaskDependsOnSource = sourceTasks.some((sourceTask) =>
1577 | taskDependsOnSource(subtask, sourceTask)
1578 | );
1579 |
1580 | if (subtaskDependsOnSource) {
1581 | return true;
1582 | }
1583 |
1584 | // Recursively check if any nested subtasks depend on source tasks
1585 | if (subtask.subtasks && Array.isArray(subtask.subtasks)) {
1586 | return subtasksDependOnSource(subtask, sourceTasks);
1587 | }
1588 |
1589 | return false;
1590 | });
1591 | }
1592 |
1593 | /**
1594 | * Get all dependent task IDs for a set of cross-tag dependencies
1595 | * @param {Array} sourceTasks - Array of source tasks
1596 | * @param {Array} crossTagDependencies - Array of cross-tag dependency conflicts
1597 | * @param {Array} allTasks - Array of all tasks from all tags
1598 | * @returns {Array} Array of dependent task IDs to move
1599 | */
1600 | function getDependentTaskIds(sourceTasks, crossTagDependencies, allTasks) {
1601 | // Enhanced parameter validation
1602 | if (!Array.isArray(sourceTasks)) {
1603 | throw new Error('Source tasks parameter must be an array');
1604 | }
1605 |
1606 | if (!Array.isArray(crossTagDependencies)) {
1607 | throw new Error('Cross tag dependencies parameter must be an array');
1608 | }
1609 |
1610 | if (!Array.isArray(allTasks)) {
1611 | throw new Error('All tasks parameter must be an array');
1612 | }
1613 |
1614 | // Use the shared recursive dependency finder
1615 | const dependentTaskIds = new Set(
1616 | findAllDependenciesRecursively(sourceTasks, allTasks, {
1617 | includeSelf: false
1618 | })
1619 | );
1620 |
1621 | // Add immediate dependency IDs from conflicts and find their dependencies recursively
1622 | const conflictTasksToProcess = [];
1623 | crossTagDependencies.forEach((conflict) => {
1624 | if (conflict && conflict.dependencyId) {
1625 | const depId =
1626 | typeof conflict.dependencyId === 'string'
1627 | ? parseInt(conflict.dependencyId, 10)
1628 | : conflict.dependencyId;
1629 | if (!isNaN(depId)) {
1630 | dependentTaskIds.add(depId);
1631 | // Find the task object for recursive dependency finding
1632 | const depTask = allTasks.find((t) => t.id === depId);
1633 | if (depTask) {
1634 | conflictTasksToProcess.push(depTask);
1635 | }
1636 | }
1637 | }
1638 | });
1639 |
1640 | // Find dependencies of conflict tasks
1641 | if (conflictTasksToProcess.length > 0) {
1642 | const conflictDependencies = findAllDependenciesRecursively(
1643 | conflictTasksToProcess,
1644 | allTasks,
1645 | { includeSelf: false }
1646 | );
1647 | conflictDependencies.forEach((depId) => dependentTaskIds.add(depId));
1648 | }
1649 |
1650 | // For --with-dependencies, we also need to find all dependencies of the source tasks
1651 | sourceTasks.forEach((sourceTask) => {
1652 | if (sourceTask && sourceTask.id) {
1653 | // Find all tasks that this source task depends on (forward dependencies) - already handled above
1654 |
1655 | // Find all tasks that depend on this source task (reverse dependencies)
1656 | findTasksThatDependOn(sourceTask.id, allTasks, dependentTaskIds);
1657 | }
1658 | });
1659 |
1660 | // Also include any tasks that depend on the source tasks
1661 | sourceTasks.forEach((sourceTask) => {
1662 | if (!sourceTask || typeof sourceTask !== 'object' || !sourceTask.id) {
1663 | return; // Skip invalid source tasks
1664 | }
1665 |
1666 | allTasks.forEach((task) => {
1667 | // Validate task and dependencies array
1668 | if (
1669 | !task ||
1670 | typeof task !== 'object' ||
1671 | !Array.isArray(task.dependencies)
1672 | ) {
1673 | return;
1674 | }
1675 |
1676 | // Check if this task depends on the source task
1677 | const hasDependency = taskDependsOnSource(task, sourceTask);
1678 |
1679 | // Check if any subtasks of this task depend on the source task
1680 | const subtasksHaveDependency = subtasksDependOnSource(task, [sourceTask]);
1681 |
1682 | if (hasDependency || subtasksHaveDependency) {
1683 | dependentTaskIds.add(task.id);
1684 | }
1685 | });
1686 | });
1687 |
1688 | return Array.from(dependentTaskIds);
1689 | }
1690 |
1691 | /**
1692 | * Validate subtask movement - block direct cross-tag subtask moves
1693 | * @param {string} taskId - Task ID to validate
1694 | * @param {string} sourceTag - Source tag name
1695 | * @param {string} targetTag - Target tag name
1696 | * @throws {Error} If subtask movement is attempted
1697 | */
1698 | function validateSubtaskMove(taskId, sourceTag, targetTag) {
1699 | // Parameter validation
1700 | if (!taskId || typeof taskId !== 'string') {
1701 | throw new DependencyError(
1702 | DEPENDENCY_ERROR_CODES.INVALID_TASK_ID,
1703 | 'Task ID must be a valid string'
1704 | );
1705 | }
1706 |
1707 | if (!sourceTag || typeof sourceTag !== 'string') {
1708 | throw new DependencyError(
1709 | DEPENDENCY_ERROR_CODES.INVALID_SOURCE_TAG,
1710 | 'Source tag must be a valid string'
1711 | );
1712 | }
1713 |
1714 | if (!targetTag || typeof targetTag !== 'string') {
1715 | throw new DependencyError(
1716 | DEPENDENCY_ERROR_CODES.INVALID_TARGET_TAG,
1717 | 'Target tag must be a valid string'
1718 | );
1719 | }
1720 |
1721 | if (taskId.includes('.')) {
1722 | throw new DependencyError(
1723 | DEPENDENCY_ERROR_CODES.CANNOT_MOVE_SUBTASK,
1724 | `Cannot move subtask ${taskId} directly between tags.
1725 |
1726 | First promote it to a full task using:
1727 | task-master remove-subtask --id=${taskId} --convert`,
1728 | {
1729 | taskId,
1730 | sourceTag,
1731 | targetTag
1732 | }
1733 | );
1734 | }
1735 | }
1736 |
1737 | /**
1738 | * Check if a task can be moved with its dependencies
1739 | * @param {string} taskId - Task ID to check
1740 | * @param {string} sourceTag - Source tag name
1741 | * @param {string} targetTag - Target tag name
1742 | * @param {Array} allTasks - Array of all tasks from all tags
1743 | * @returns {Object} Object with canMove boolean and dependentTaskIds array
1744 | */
1745 | function canMoveWithDependencies(taskId, sourceTag, targetTag, allTasks) {
1746 | // Parameter validation
1747 | if (!taskId || typeof taskId !== 'string') {
1748 | throw new Error('Task ID must be a valid string');
1749 | }
1750 |
1751 | if (!sourceTag || typeof sourceTag !== 'string') {
1752 | throw new Error('Source tag must be a valid string');
1753 | }
1754 |
1755 | if (!targetTag || typeof targetTag !== 'string') {
1756 | throw new Error('Target tag must be a valid string');
1757 | }
1758 |
1759 | if (!Array.isArray(allTasks)) {
1760 | throw new Error('All tasks parameter must be an array');
1761 | }
1762 |
1763 | // Enhanced task lookup to handle subtasks properly
1764 | let sourceTask = null;
1765 |
1766 | // Check if it's a subtask ID (e.g., "1.2")
1767 | if (taskId.includes('.')) {
1768 | const [parentId, subtaskId] = taskId
1769 | .split('.')
1770 | .map((id) => parseInt(id, 10));
1771 | const parentTask = allTasks.find(
1772 | (t) => t.id === parentId && t.tag === sourceTag
1773 | );
1774 |
1775 | if (
1776 | parentTask &&
1777 | parentTask.subtasks &&
1778 | Array.isArray(parentTask.subtasks)
1779 | ) {
1780 | const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
1781 | if (subtask) {
1782 | // Create a copy of the subtask with parent context
1783 | sourceTask = {
1784 | ...subtask,
1785 | parentTask: {
1786 | id: parentTask.id,
1787 | title: parentTask.title,
1788 | status: parentTask.status
1789 | },
1790 | isSubtask: true
1791 | };
1792 | }
1793 | }
1794 | } else {
1795 | // Regular task lookup - handle both string and numeric IDs
1796 | sourceTask = allTasks.find((t) => {
1797 | const taskIdNum = parseInt(taskId, 10);
1798 | return (t.id === taskIdNum || t.id === taskId) && t.tag === sourceTag;
1799 | });
1800 | }
1801 |
1802 | if (!sourceTask) {
1803 | return {
1804 | canMove: false,
1805 | dependentTaskIds: [],
1806 | conflicts: [],
1807 | error: 'Task not found'
1808 | };
1809 | }
1810 |
1811 | const validation = validateCrossTagMove(
1812 | sourceTask,
1813 | sourceTag,
1814 | targetTag,
1815 | allTasks
1816 | );
1817 |
1818 | // Fix contradictory logic: return canMove: false when conflicts exist
1819 | if (validation.canMove) {
1820 | return {
1821 | canMove: true,
1822 | dependentTaskIds: [],
1823 | conflicts: []
1824 | };
1825 | }
1826 |
1827 | // When conflicts exist, return canMove: false with conflicts and dependent task IDs
1828 | const dependentTaskIds = getDependentTaskIds(
1829 | [sourceTask],
1830 | validation.conflicts,
1831 | allTasks
1832 | );
1833 |
1834 | return {
1835 | canMove: false,
1836 | dependentTaskIds,
1837 | conflicts: validation.conflicts
1838 | };
1839 | }
1840 |
1841 | export {
1842 | addDependency,
1843 | removeDependency,
1844 | isCircularDependency,
1845 | validateTaskDependencies,
1846 | validateDependenciesCommand,
1847 | fixDependenciesCommand,
1848 | removeDuplicateDependencies,
1849 | cleanupSubtaskDependencies,
1850 | ensureAtLeastOneIndependentSubtask,
1851 | validateAndFixDependencies,
1852 | findDependencyTask,
1853 | findTaskCrossTagConflicts,
1854 | validateCrossTagMove,
1855 | findCrossTagDependencies,
1856 | getDependentTaskIds,
1857 | validateSubtaskMove,
1858 | canMoveWithDependencies,
1859 | findAllDependenciesRecursively,
1860 | DependencyError,
1861 | DEPENDENCY_ERROR_CODES
1862 | };
1863 |
```