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