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