This is page 41 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 -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/expand-task.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Tests for the expand-task.js module 3 | */ 4 | import { jest } from '@jest/globals'; 5 | import fs from 'fs'; 6 | import { 7 | createGetTagAwareFilePathMock, 8 | createSlugifyTagForFilePathMock 9 | } from './setup.js'; 10 | 11 | // Mock the dependencies before importing the module under test 12 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ 13 | readJSON: jest.fn(), 14 | writeJSON: jest.fn(), 15 | log: jest.fn(), 16 | CONFIG: { 17 | model: 'mock-claude-model', 18 | maxTokens: 4000, 19 | temperature: 0.7, 20 | debug: false 21 | }, 22 | sanitizePrompt: jest.fn((prompt) => prompt), 23 | truncate: jest.fn((text) => text), 24 | isSilentMode: jest.fn(() => false), 25 | findTaskById: jest.fn(), 26 | findProjectRoot: jest.fn((tasksPath) => '/mock/project/root'), 27 | getCurrentTag: jest.fn(() => 'master'), 28 | ensureTagMetadata: jest.fn((tagObj) => tagObj), 29 | flattenTasksWithSubtasks: jest.fn((tasks) => { 30 | const allTasks = []; 31 | const queue = [...(tasks || [])]; 32 | while (queue.length > 0) { 33 | const task = queue.shift(); 34 | allTasks.push(task); 35 | if (task.subtasks) { 36 | for (const subtask of task.subtasks) { 37 | queue.push({ ...subtask, id: `${task.id}.${subtask.id}` }); 38 | } 39 | } 40 | } 41 | return allTasks; 42 | }), 43 | getTagAwareFilePath: createGetTagAwareFilePathMock(), 44 | slugifyTagForFilePath: createSlugifyTagForFilePathMock(), 45 | readComplexityReport: jest.fn(), 46 | markMigrationForNotice: jest.fn(), 47 | performCompleteTagMigration: jest.fn(), 48 | setTasksForTag: jest.fn(), 49 | getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || []) 50 | })); 51 | 52 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({ 53 | displayBanner: jest.fn(), 54 | getStatusWithColor: jest.fn((status) => status), 55 | startLoadingIndicator: jest.fn(), 56 | stopLoadingIndicator: jest.fn(), 57 | succeedLoadingIndicator: jest.fn(), 58 | failLoadingIndicator: jest.fn(), 59 | warnLoadingIndicator: jest.fn(), 60 | infoLoadingIndicator: jest.fn(), 61 | displayAiUsageSummary: jest.fn(), 62 | displayContextAnalysis: jest.fn() 63 | })); 64 | 65 | jest.unstable_mockModule( 66 | '../../../../../scripts/modules/ai-services-unified.js', 67 | () => ({ 68 | generateTextService: jest.fn().mockResolvedValue({ 69 | mainResult: JSON.stringify({ 70 | subtasks: [ 71 | { 72 | id: 1, 73 | title: 'Set up project structure', 74 | description: 75 | 'Create the basic project directory structure and configuration files', 76 | dependencies: [], 77 | details: 78 | 'Initialize package.json, create src/ and test/ directories, set up linting configuration', 79 | status: 'pending', 80 | testStrategy: 81 | 'Verify all expected files and directories are created' 82 | }, 83 | { 84 | id: 2, 85 | title: 'Implement core functionality', 86 | description: 'Develop the main application logic and core features', 87 | dependencies: [1], 88 | details: 89 | 'Create main classes, implement business logic, set up data models', 90 | status: 'pending', 91 | testStrategy: 'Unit tests for all core functions and classes' 92 | }, 93 | { 94 | id: 3, 95 | title: 'Add user interface', 96 | description: 'Create the user interface components and layouts', 97 | dependencies: [2], 98 | details: 99 | 'Design UI components, implement responsive layouts, add user interactions', 100 | status: 'pending', 101 | testStrategy: 'UI tests and visual regression testing' 102 | } 103 | ] 104 | }), 105 | telemetryData: { 106 | timestamp: new Date().toISOString(), 107 | userId: '1234567890', 108 | commandName: 'expand-task', 109 | modelUsed: 'claude-3-5-sonnet', 110 | providerName: 'anthropic', 111 | inputTokens: 1000, 112 | outputTokens: 500, 113 | totalTokens: 1500, 114 | totalCost: 0.012414, 115 | currency: 'USD' 116 | } 117 | }) 118 | }) 119 | ); 120 | 121 | jest.unstable_mockModule( 122 | '../../../../../scripts/modules/config-manager.js', 123 | () => ({ 124 | getDefaultSubtasks: jest.fn(() => 3), 125 | getDebugFlag: jest.fn(() => false), 126 | getDefaultNumTasks: jest.fn(() => 10), 127 | getMainProvider: jest.fn(() => 'openai'), 128 | getResearchProvider: jest.fn(() => 'perplexity'), 129 | hasCodebaseAnalysis: jest.fn(() => false) 130 | }) 131 | ); 132 | 133 | jest.unstable_mockModule( 134 | '../../../../../scripts/modules/utils/contextGatherer.js', 135 | () => ({ 136 | ContextGatherer: jest.fn().mockImplementation(() => ({ 137 | gather: jest.fn().mockResolvedValue({ 138 | context: 'Mock project context from files' 139 | }) 140 | })) 141 | }) 142 | ); 143 | 144 | jest.unstable_mockModule( 145 | '../../../../../scripts/modules/utils/fuzzyTaskSearch.js', 146 | () => ({ 147 | FuzzyTaskSearch: jest.fn().mockImplementation(() => ({ 148 | findRelevantTasks: jest.fn().mockReturnValue([]), 149 | getTaskIds: jest.fn().mockReturnValue([]) 150 | })) 151 | }) 152 | ); 153 | 154 | jest.unstable_mockModule( 155 | '../../../../../scripts/modules/task-manager/generate-task-files.js', 156 | () => ({ 157 | default: jest.fn().mockResolvedValue() 158 | }) 159 | ); 160 | 161 | jest.unstable_mockModule( 162 | '../../../../../scripts/modules/prompt-manager.js', 163 | () => ({ 164 | getPromptManager: jest.fn().mockReturnValue({ 165 | loadPrompt: jest.fn().mockResolvedValue({ 166 | systemPrompt: 'Mocked system prompt', 167 | userPrompt: 'Mocked user prompt' 168 | }) 169 | }) 170 | }) 171 | ); 172 | 173 | // Mock external UI libraries 174 | jest.unstable_mockModule('chalk', () => ({ 175 | default: { 176 | white: { bold: jest.fn((text) => text) }, 177 | cyan: Object.assign( 178 | jest.fn((text) => text), 179 | { 180 | bold: jest.fn((text) => text) 181 | } 182 | ), 183 | green: jest.fn((text) => text), 184 | yellow: jest.fn((text) => text), 185 | bold: jest.fn((text) => text) 186 | } 187 | })); 188 | 189 | jest.unstable_mockModule('boxen', () => ({ 190 | default: jest.fn((text) => text) 191 | })); 192 | 193 | jest.unstable_mockModule('cli-table3', () => ({ 194 | default: jest.fn().mockImplementation(() => ({ 195 | push: jest.fn(), 196 | toString: jest.fn(() => 'mocked table') 197 | })) 198 | })); 199 | 200 | // Mock process.exit to prevent Jest worker crashes 201 | const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => { 202 | throw new Error(`process.exit called with "${code}"`); 203 | }); 204 | 205 | // Import the mocked modules 206 | const { 207 | readJSON, 208 | writeJSON, 209 | log, 210 | findTaskById, 211 | ensureTagMetadata, 212 | readComplexityReport, 213 | findProjectRoot 214 | } = await import('../../../../../scripts/modules/utils.js'); 215 | 216 | const { generateTextService } = await import( 217 | '../../../../../scripts/modules/ai-services-unified.js' 218 | ); 219 | 220 | const generateTaskFiles = ( 221 | await import( 222 | '../../../../../scripts/modules/task-manager/generate-task-files.js' 223 | ) 224 | ).default; 225 | 226 | const { getDefaultSubtasks } = await import( 227 | '../../../../../scripts/modules/config-manager.js' 228 | ); 229 | 230 | // Import the module under test 231 | const { default: expandTask } = await import( 232 | '../../../../../scripts/modules/task-manager/expand-task.js' 233 | ); 234 | 235 | describe('expandTask', () => { 236 | const sampleTasks = { 237 | master: { 238 | tasks: [ 239 | { 240 | id: 1, 241 | title: 'Task 1', 242 | description: 'First task', 243 | status: 'done', 244 | dependencies: [], 245 | details: 'Already completed task', 246 | subtasks: [] 247 | }, 248 | { 249 | id: 2, 250 | title: 'Task 2', 251 | description: 'Second task', 252 | status: 'pending', 253 | dependencies: [], 254 | details: 'Task ready for expansion', 255 | subtasks: [] 256 | }, 257 | { 258 | id: 3, 259 | title: 'Complex Task', 260 | description: 'A complex task that needs breakdown', 261 | status: 'pending', 262 | dependencies: [1], 263 | details: 'This task involves multiple steps', 264 | subtasks: [] 265 | }, 266 | { 267 | id: 4, 268 | title: 'Task with existing subtasks', 269 | description: 'Task that already has subtasks', 270 | status: 'pending', 271 | dependencies: [], 272 | details: 'Has existing subtasks', 273 | subtasks: [ 274 | { 275 | id: 1, 276 | title: 'Existing subtask', 277 | description: 'Already exists', 278 | status: 'pending', 279 | dependencies: [] 280 | } 281 | ] 282 | } 283 | ] 284 | }, 285 | 'feature-branch': { 286 | tasks: [ 287 | { 288 | id: 1, 289 | title: 'Feature Task 1', 290 | description: 'Task in feature branch', 291 | status: 'pending', 292 | dependencies: [], 293 | details: 'Feature-specific task', 294 | subtasks: [] 295 | } 296 | ] 297 | } 298 | }; 299 | 300 | // Create a helper function for consistent mcpLog mock 301 | const createMcpLogMock = () => ({ 302 | info: jest.fn(), 303 | warn: jest.fn(), 304 | error: jest.fn(), 305 | debug: jest.fn(), 306 | success: jest.fn() 307 | }); 308 | 309 | beforeEach(() => { 310 | jest.clearAllMocks(); 311 | mockExit.mockClear(); 312 | 313 | // Default readJSON implementation - returns tagged structure 314 | readJSON.mockImplementation((tasksPath, projectRoot, tag) => { 315 | const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks)); 316 | const selectedTag = tag || 'master'; 317 | return { 318 | ...sampleTasksCopy[selectedTag], 319 | tag: selectedTag, 320 | _rawTaggedData: sampleTasksCopy 321 | }; 322 | }); 323 | 324 | // Default findTaskById implementation 325 | findTaskById.mockImplementation((tasks, taskId) => { 326 | const id = parseInt(taskId, 10); 327 | return tasks.find((t) => t.id === id); 328 | }); 329 | 330 | // Default complexity report (no report available) 331 | readComplexityReport.mockReturnValue(null); 332 | 333 | // Mock findProjectRoot to return consistent path for complexity report 334 | findProjectRoot.mockReturnValue('/mock/project/root'); 335 | 336 | writeJSON.mockResolvedValue(); 337 | generateTaskFiles.mockResolvedValue(); 338 | log.mockImplementation(() => {}); 339 | 340 | // Mock console.log to avoid output during tests 341 | jest.spyOn(console, 'log').mockImplementation(() => {}); 342 | }); 343 | 344 | afterEach(() => { 345 | console.log.mockRestore(); 346 | }); 347 | 348 | describe('Basic Functionality', () => { 349 | test('should expand a task with AI-generated subtasks', async () => { 350 | // Arrange 351 | const tasksPath = 'tasks/tasks.json'; 352 | const taskId = '2'; 353 | const numSubtasks = 3; 354 | const context = { 355 | mcpLog: createMcpLogMock(), 356 | projectRoot: '/mock/project/root' 357 | }; 358 | 359 | // Act 360 | const result = await expandTask( 361 | tasksPath, 362 | taskId, 363 | numSubtasks, 364 | false, 365 | '', 366 | context, 367 | false 368 | ); 369 | 370 | // Assert 371 | expect(readJSON).toHaveBeenCalledWith( 372 | tasksPath, 373 | '/mock/project/root', 374 | undefined 375 | ); 376 | expect(generateTextService).toHaveBeenCalledWith(expect.any(Object)); 377 | expect(writeJSON).toHaveBeenCalledWith( 378 | tasksPath, 379 | expect.objectContaining({ 380 | tasks: expect.arrayContaining([ 381 | expect.objectContaining({ 382 | id: 2, 383 | subtasks: expect.arrayContaining([ 384 | expect.objectContaining({ 385 | id: 1, 386 | title: 'Set up project structure', 387 | status: 'pending' 388 | }), 389 | expect.objectContaining({ 390 | id: 2, 391 | title: 'Implement core functionality', 392 | status: 'pending' 393 | }), 394 | expect.objectContaining({ 395 | id: 3, 396 | title: 'Add user interface', 397 | status: 'pending' 398 | }) 399 | ]) 400 | }) 401 | ]), 402 | tag: 'master', 403 | _rawTaggedData: expect.objectContaining({ 404 | master: expect.objectContaining({ 405 | tasks: expect.any(Array) 406 | }) 407 | }) 408 | }), 409 | '/mock/project/root', 410 | undefined 411 | ); 412 | expect(result).toEqual( 413 | expect.objectContaining({ 414 | task: expect.objectContaining({ 415 | id: 2, 416 | subtasks: expect.arrayContaining([ 417 | expect.objectContaining({ 418 | id: 1, 419 | title: 'Set up project structure', 420 | status: 'pending' 421 | }), 422 | expect.objectContaining({ 423 | id: 2, 424 | title: 'Implement core functionality', 425 | status: 'pending' 426 | }), 427 | expect.objectContaining({ 428 | id: 3, 429 | title: 'Add user interface', 430 | status: 'pending' 431 | }) 432 | ]) 433 | }), 434 | telemetryData: expect.any(Object) 435 | }) 436 | ); 437 | }); 438 | 439 | test('should handle research flag correctly', async () => { 440 | // Arrange 441 | const tasksPath = 'tasks/tasks.json'; 442 | const taskId = '2'; 443 | const numSubtasks = 3; 444 | const context = { 445 | mcpLog: createMcpLogMock(), 446 | projectRoot: '/mock/project/root' 447 | }; 448 | 449 | // Act 450 | await expandTask( 451 | tasksPath, 452 | taskId, 453 | numSubtasks, 454 | true, // useResearch = true 455 | 'Additional context for research', 456 | context, 457 | false 458 | ); 459 | 460 | // Assert 461 | expect(generateTextService).toHaveBeenCalledWith( 462 | expect.objectContaining({ 463 | role: 'research', 464 | commandName: expect.any(String) 465 | }) 466 | ); 467 | }); 468 | 469 | test('should handle complexity report integration without errors', async () => { 470 | // Arrange 471 | const tasksPath = 'tasks/tasks.json'; 472 | const taskId = '2'; 473 | const context = { 474 | mcpLog: createMcpLogMock(), 475 | projectRoot: '/mock/project/root' 476 | }; 477 | 478 | // Act & Assert - Should complete without errors 479 | const result = await expandTask( 480 | tasksPath, 481 | taskId, 482 | undefined, // numSubtasks not specified 483 | false, 484 | '', 485 | context, 486 | false 487 | ); 488 | 489 | // Assert - Should successfully expand and return expected structure 490 | expect(result).toEqual( 491 | expect.objectContaining({ 492 | task: expect.objectContaining({ 493 | id: 2, 494 | subtasks: expect.any(Array) 495 | }), 496 | telemetryData: expect.any(Object) 497 | }) 498 | ); 499 | expect(generateTextService).toHaveBeenCalled(); 500 | }); 501 | }); 502 | 503 | describe('Tag Handling (The Critical Bug Fix)', () => { 504 | test('should preserve tagged structure when expanding with default tag', async () => { 505 | // Arrange 506 | const tasksPath = 'tasks/tasks.json'; 507 | const taskId = '2'; 508 | const context = { 509 | mcpLog: createMcpLogMock(), 510 | projectRoot: '/mock/project/root', 511 | tag: 'master' // Explicit tag context 512 | }; 513 | 514 | // Act 515 | await expandTask(tasksPath, taskId, 3, false, '', context, false); 516 | 517 | // Assert - CRITICAL: Check tag is passed to readJSON and writeJSON 518 | expect(readJSON).toHaveBeenCalledWith( 519 | tasksPath, 520 | '/mock/project/root', 521 | 'master' 522 | ); 523 | expect(writeJSON).toHaveBeenCalledWith( 524 | tasksPath, 525 | expect.objectContaining({ 526 | tag: 'master', 527 | _rawTaggedData: expect.objectContaining({ 528 | master: expect.any(Object), 529 | 'feature-branch': expect.any(Object) 530 | }) 531 | }), 532 | '/mock/project/root', 533 | 'master' // CRITICAL: Tag must be passed to writeJSON 534 | ); 535 | }); 536 | 537 | test('should preserve tagged structure when expanding with non-default tag', async () => { 538 | // Arrange 539 | const tasksPath = 'tasks/tasks.json'; 540 | const taskId = '1'; // Task in feature-branch 541 | const context = { 542 | mcpLog: createMcpLogMock(), 543 | projectRoot: '/mock/project/root', 544 | tag: 'feature-branch' // Different tag context 545 | }; 546 | 547 | // Configure readJSON to return feature-branch data 548 | readJSON.mockImplementation((tasksPath, projectRoot, tag) => { 549 | const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks)); 550 | return { 551 | ...sampleTasksCopy['feature-branch'], 552 | tag: 'feature-branch', 553 | _rawTaggedData: sampleTasksCopy 554 | }; 555 | }); 556 | 557 | // Act 558 | await expandTask(tasksPath, taskId, 3, false, '', context, false); 559 | 560 | // Assert - CRITICAL: Check tag preservation for non-default tag 561 | expect(readJSON).toHaveBeenCalledWith( 562 | tasksPath, 563 | '/mock/project/root', 564 | 'feature-branch' 565 | ); 566 | expect(writeJSON).toHaveBeenCalledWith( 567 | tasksPath, 568 | expect.objectContaining({ 569 | tag: 'feature-branch', 570 | _rawTaggedData: expect.objectContaining({ 571 | master: expect.any(Object), 572 | 'feature-branch': expect.any(Object) 573 | }) 574 | }), 575 | '/mock/project/root', 576 | 'feature-branch' // CRITICAL: Correct tag passed to writeJSON 577 | ); 578 | }); 579 | 580 | test('should NOT corrupt tagged structure when tag is undefined', async () => { 581 | // Arrange 582 | const tasksPath = 'tasks/tasks.json'; 583 | const taskId = '2'; 584 | const context = { 585 | mcpLog: createMcpLogMock(), 586 | projectRoot: '/mock/project/root' 587 | // No tag specified - should default gracefully 588 | }; 589 | 590 | // Act 591 | await expandTask(tasksPath, taskId, 3, false, '', context, false); 592 | 593 | // Assert - Should still preserve structure with undefined tag 594 | expect(readJSON).toHaveBeenCalledWith( 595 | tasksPath, 596 | '/mock/project/root', 597 | undefined 598 | ); 599 | expect(writeJSON).toHaveBeenCalledWith( 600 | tasksPath, 601 | expect.objectContaining({ 602 | _rawTaggedData: expect.objectContaining({ 603 | master: expect.any(Object) 604 | }) 605 | }), 606 | '/mock/project/root', 607 | undefined 608 | ); 609 | 610 | // CRITICAL: Verify structure is NOT flattened to old format 611 | const writeCallArgs = writeJSON.mock.calls[0][1]; 612 | expect(writeCallArgs).toHaveProperty('tasks'); // Should have tasks property from readJSON mock 613 | expect(writeCallArgs).toHaveProperty('_rawTaggedData'); // Should preserve tagged structure 614 | }); 615 | }); 616 | 617 | describe('Force Flag Handling', () => { 618 | test('should replace existing subtasks when force=true', async () => { 619 | // Arrange 620 | const tasksPath = 'tasks/tasks.json'; 621 | const taskId = '4'; // Task with existing subtasks 622 | const context = { 623 | mcpLog: createMcpLogMock(), 624 | projectRoot: '/mock/project/root' 625 | }; 626 | 627 | // Act 628 | await expandTask(tasksPath, taskId, 3, false, '', context, true); 629 | 630 | // Assert - Should replace existing subtasks 631 | expect(writeJSON).toHaveBeenCalledWith( 632 | tasksPath, 633 | expect.objectContaining({ 634 | tasks: expect.arrayContaining([ 635 | expect.objectContaining({ 636 | id: 4, 637 | subtasks: expect.arrayContaining([ 638 | expect.objectContaining({ 639 | id: 1, 640 | title: 'Set up project structure' 641 | }) 642 | ]) 643 | }) 644 | ]) 645 | }), 646 | '/mock/project/root', 647 | undefined 648 | ); 649 | }); 650 | 651 | test('should append to existing subtasks when force=false', async () => { 652 | // Arrange 653 | const tasksPath = 'tasks/tasks.json'; 654 | const taskId = '4'; // Task with existing subtasks 655 | const context = { 656 | mcpLog: createMcpLogMock(), 657 | projectRoot: '/mock/project/root' 658 | }; 659 | 660 | // Act 661 | await expandTask(tasksPath, taskId, 3, false, '', context, false); 662 | 663 | // Assert - Should append to existing subtasks with proper ID increments 664 | expect(writeJSON).toHaveBeenCalledWith( 665 | tasksPath, 666 | expect.objectContaining({ 667 | tasks: expect.arrayContaining([ 668 | expect.objectContaining({ 669 | id: 4, 670 | subtasks: expect.arrayContaining([ 671 | // Should contain both existing and new subtasks 672 | expect.any(Object), 673 | expect.any(Object), 674 | expect.any(Object), 675 | expect.any(Object) // 1 existing + 3 new = 4 total 676 | ]) 677 | }) 678 | ]) 679 | }), 680 | '/mock/project/root', 681 | undefined 682 | ); 683 | }); 684 | }); 685 | 686 | describe('Complexity Report Integration (Tag-Specific)', () => { 687 | test('should use tag-specific complexity report when available', async () => { 688 | // Arrange 689 | const { getPromptManager } = await import( 690 | '../../../../../scripts/modules/prompt-manager.js' 691 | ); 692 | const mockLoadPrompt = jest.fn().mockResolvedValue({ 693 | systemPrompt: 'Generate exactly 5 subtasks for complexity report', 694 | userPrompt: 695 | 'Please break this task into 5 parts\n\nUser provided context' 696 | }); 697 | getPromptManager.mockReturnValue({ 698 | loadPrompt: mockLoadPrompt 699 | }); 700 | 701 | const tasksPath = 'tasks/tasks.json'; 702 | const taskId = '1'; // Task in feature-branch 703 | const context = { 704 | mcpLog: createMcpLogMock(), 705 | projectRoot: '/mock/project/root', 706 | tag: 'feature-branch', 707 | complexityReportPath: 708 | '/mock/project/root/task-complexity-report_feature-branch.json' 709 | }; 710 | 711 | // Stub fs.existsSync to simulate complexity report exists for this tag 712 | const existsSpy = jest 713 | .spyOn(fs, 'existsSync') 714 | .mockImplementation((filepath) => 715 | filepath.endsWith('task-complexity-report_feature-branch.json') 716 | ); 717 | 718 | // Stub readJSON to return complexity report when reading the report path 719 | readJSON.mockImplementation((filepath, projectRootParam, tagParam) => { 720 | if (filepath.includes('task-complexity-report_feature-branch.json')) { 721 | return { 722 | complexityAnalysis: [ 723 | { 724 | taskId: 1, 725 | complexityScore: 8, 726 | recommendedSubtasks: 5, 727 | reasoning: 'Needs five detailed steps', 728 | expansionPrompt: 'Please break this task into 5 parts' 729 | } 730 | ] 731 | }; 732 | } 733 | // Default tasks data for tasks.json 734 | const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks)); 735 | const selectedTag = tagParam || 'master'; 736 | return { 737 | ...sampleTasksCopy[selectedTag], 738 | tag: selectedTag, 739 | _rawTaggedData: sampleTasksCopy 740 | }; 741 | }); 742 | 743 | // Act 744 | await expandTask(tasksPath, taskId, undefined, false, '', context, false); 745 | 746 | // Assert - generateTextService called with systemPrompt for 5 subtasks 747 | const callArg = generateTextService.mock.calls[0][0]; 748 | expect(callArg.systemPrompt).toContain('Generate exactly 5 subtasks'); 749 | 750 | // Assert - Should use complexity-report variant with expansion prompt 751 | expect(mockLoadPrompt).toHaveBeenCalledWith( 752 | 'expand-task', 753 | expect.objectContaining({ 754 | subtaskCount: 5, 755 | expansionPrompt: 'Please break this task into 5 parts' 756 | }), 757 | 'complexity-report' 758 | ); 759 | 760 | // Clean up stub 761 | existsSpy.mockRestore(); 762 | }); 763 | }); 764 | 765 | describe('Error Handling', () => { 766 | test('should handle non-existent task ID', async () => { 767 | // Arrange 768 | const tasksPath = 'tasks/tasks.json'; 769 | const taskId = '999'; // Non-existent task 770 | const context = { 771 | mcpLog: createMcpLogMock(), 772 | projectRoot: '/mock/project/root' 773 | }; 774 | 775 | findTaskById.mockReturnValue(null); 776 | 777 | // Act & Assert 778 | await expect( 779 | expandTask(tasksPath, taskId, 3, false, '', context, false) 780 | ).rejects.toThrow('Task 999 not found'); 781 | 782 | expect(writeJSON).not.toHaveBeenCalled(); 783 | }); 784 | 785 | test('should expand tasks regardless of status (including done tasks)', async () => { 786 | // Arrange 787 | const tasksPath = 'tasks/tasks.json'; 788 | const taskId = '1'; // Task with 'done' status 789 | const context = { 790 | mcpLog: createMcpLogMock(), 791 | projectRoot: '/mock/project/root' 792 | }; 793 | 794 | // Act 795 | const result = await expandTask( 796 | tasksPath, 797 | taskId, 798 | 3, 799 | false, 800 | '', 801 | context, 802 | false 803 | ); 804 | 805 | // Assert - Should successfully expand even 'done' tasks 806 | expect(writeJSON).toHaveBeenCalled(); 807 | expect(result).toEqual( 808 | expect.objectContaining({ 809 | task: expect.objectContaining({ 810 | id: 1, 811 | status: 'done', // Status unchanged 812 | subtasks: expect.arrayContaining([ 813 | expect.objectContaining({ 814 | id: 1, 815 | title: 'Set up project structure', 816 | status: 'pending' 817 | }) 818 | ]) 819 | }), 820 | telemetryData: expect.any(Object) 821 | }) 822 | ); 823 | }); 824 | 825 | test('should handle AI service failures', async () => { 826 | // Arrange 827 | const tasksPath = 'tasks/tasks.json'; 828 | const taskId = '2'; 829 | const context = { 830 | mcpLog: createMcpLogMock(), 831 | projectRoot: '/mock/project/root' 832 | }; 833 | 834 | generateTextService.mockRejectedValueOnce(new Error('AI service error')); 835 | 836 | // Act & Assert 837 | await expect( 838 | expandTask(tasksPath, taskId, 3, false, '', context, false) 839 | ).rejects.toThrow('AI service error'); 840 | 841 | expect(writeJSON).not.toHaveBeenCalled(); 842 | }); 843 | 844 | test('should handle file read errors', async () => { 845 | // Arrange 846 | const tasksPath = 'tasks/tasks.json'; 847 | const taskId = '2'; 848 | const context = { 849 | mcpLog: createMcpLogMock(), 850 | projectRoot: '/mock/project/root' 851 | }; 852 | 853 | readJSON.mockImplementation(() => { 854 | throw new Error('File read failed'); 855 | }); 856 | 857 | // Act & Assert 858 | await expect( 859 | expandTask(tasksPath, taskId, 3, false, '', context, false) 860 | ).rejects.toThrow('File read failed'); 861 | 862 | expect(writeJSON).not.toHaveBeenCalled(); 863 | }); 864 | 865 | test('should handle invalid tasks data', async () => { 866 | // Arrange 867 | const tasksPath = 'tasks/tasks.json'; 868 | const taskId = '2'; 869 | const context = { 870 | mcpLog: createMcpLogMock(), 871 | projectRoot: '/mock/project/root' 872 | }; 873 | 874 | readJSON.mockReturnValue(null); 875 | 876 | // Act & Assert 877 | await expect( 878 | expandTask(tasksPath, taskId, 3, false, '', context, false) 879 | ).rejects.toThrow(); 880 | }); 881 | }); 882 | 883 | describe('Output Format Handling', () => { 884 | test('should display telemetry for CLI output format', async () => { 885 | // Arrange 886 | const { displayAiUsageSummary } = await import( 887 | '../../../../../scripts/modules/ui.js' 888 | ); 889 | const tasksPath = 'tasks/tasks.json'; 890 | const taskId = '2'; 891 | const context = { 892 | projectRoot: '/mock/project/root' 893 | // No mcpLog - should trigger CLI mode 894 | }; 895 | 896 | // Act 897 | await expandTask(tasksPath, taskId, 3, false, '', context, false); 898 | 899 | // Assert - Should display telemetry for CLI users 900 | expect(displayAiUsageSummary).toHaveBeenCalledWith( 901 | expect.objectContaining({ 902 | commandName: 'expand-task', 903 | modelUsed: 'claude-3-5-sonnet', 904 | totalCost: 0.012414 905 | }), 906 | 'cli' 907 | ); 908 | }); 909 | 910 | test('should not display telemetry for MCP output format', async () => { 911 | // Arrange 912 | const { displayAiUsageSummary } = await import( 913 | '../../../../../scripts/modules/ui.js' 914 | ); 915 | const tasksPath = 'tasks/tasks.json'; 916 | const taskId = '2'; 917 | const context = { 918 | mcpLog: createMcpLogMock(), 919 | projectRoot: '/mock/project/root' 920 | }; 921 | 922 | // Act 923 | await expandTask(tasksPath, taskId, 3, false, '', context, false); 924 | 925 | // Assert - Should NOT display telemetry for MCP (handled at higher level) 926 | expect(displayAiUsageSummary).not.toHaveBeenCalled(); 927 | }); 928 | }); 929 | 930 | describe('Edge Cases', () => { 931 | test('should handle empty additional context', async () => { 932 | // Arrange 933 | const tasksPath = 'tasks/tasks.json'; 934 | const taskId = '2'; 935 | const context = { 936 | mcpLog: createMcpLogMock(), 937 | projectRoot: '/mock/project/root' 938 | }; 939 | 940 | // Act 941 | await expandTask(tasksPath, taskId, 3, false, '', context, false); 942 | 943 | // Assert - Should work with empty context (but may include project context) 944 | expect(generateTextService).toHaveBeenCalledWith( 945 | expect.objectContaining({ 946 | prompt: expect.stringMatching(/.*/) // Just ensure prompt exists 947 | }) 948 | ); 949 | }); 950 | 951 | test('should handle additional context correctly', async () => { 952 | // Arrange 953 | const { getPromptManager } = await import( 954 | '../../../../../scripts/modules/prompt-manager.js' 955 | ); 956 | const mockLoadPrompt = jest.fn().mockResolvedValue({ 957 | systemPrompt: 'Mocked system prompt', 958 | userPrompt: 'Mocked user prompt with context' 959 | }); 960 | getPromptManager.mockReturnValue({ 961 | loadPrompt: mockLoadPrompt 962 | }); 963 | 964 | const tasksPath = 'tasks/tasks.json'; 965 | const taskId = '2'; 966 | const additionalContext = 'Use React hooks and TypeScript'; 967 | const context = { 968 | mcpLog: createMcpLogMock(), 969 | projectRoot: '/mock/project/root' 970 | }; 971 | 972 | // Act 973 | await expandTask( 974 | tasksPath, 975 | taskId, 976 | 3, 977 | false, 978 | additionalContext, 979 | context, 980 | false 981 | ); 982 | 983 | // Assert - Should pass separate context parameters to prompt manager 984 | expect(mockLoadPrompt).toHaveBeenCalledWith( 985 | 'expand-task', 986 | expect.objectContaining({ 987 | additionalContext: expect.stringContaining( 988 | 'Use React hooks and TypeScript' 989 | ), 990 | gatheredContext: expect.stringContaining( 991 | 'Mock project context from files' 992 | ) 993 | }), 994 | expect.any(String) 995 | ); 996 | 997 | // Additional assertion to verify the context parameters are passed separately 998 | const call = mockLoadPrompt.mock.calls[0]; 999 | const parameters = call[1]; 1000 | expect(parameters.additionalContext).toContain( 1001 | 'Use React hooks and TypeScript' 1002 | ); 1003 | expect(parameters.gatheredContext).toContain( 1004 | 'Mock project context from files' 1005 | ); 1006 | }); 1007 | 1008 | test('should handle missing project root in context', async () => { 1009 | // Arrange 1010 | const tasksPath = 'tasks/tasks.json'; 1011 | const taskId = '2'; 1012 | const context = { 1013 | mcpLog: createMcpLogMock() 1014 | // No projectRoot in context 1015 | }; 1016 | 1017 | // Act 1018 | await expandTask(tasksPath, taskId, 3, false, '', context, false); 1019 | 1020 | // Assert - Should derive project root from tasksPath 1021 | expect(findProjectRoot).toHaveBeenCalledWith(tasksPath); 1022 | expect(readJSON).toHaveBeenCalledWith( 1023 | tasksPath, 1024 | '/mock/project/root', 1025 | undefined 1026 | ); 1027 | }); 1028 | }); 1029 | 1030 | describe('Dynamic Subtask Generation', () => { 1031 | const tasksPath = 'tasks/tasks.json'; 1032 | const taskId = 1; 1033 | const context = { session: null, mcpLog: null }; 1034 | 1035 | beforeEach(() => { 1036 | // Reset all mocks 1037 | jest.clearAllMocks(); 1038 | 1039 | // Setup default mocks 1040 | readJSON.mockReturnValue({ 1041 | tasks: [ 1042 | { 1043 | id: 1, 1044 | title: 'Test Task', 1045 | description: 'A test task', 1046 | status: 'pending', 1047 | subtasks: [] 1048 | } 1049 | ] 1050 | }); 1051 | 1052 | findTaskById.mockReturnValue({ 1053 | id: 1, 1054 | title: 'Test Task', 1055 | description: 'A test task', 1056 | status: 'pending', 1057 | subtasks: [] 1058 | }); 1059 | 1060 | findProjectRoot.mockReturnValue('/mock/project/root'); 1061 | }); 1062 | 1063 | test('should accept 0 as valid numSubtasks value for dynamic generation', async () => { 1064 | // Act - Call with numSubtasks=0 (should not throw error) 1065 | const result = await expandTask( 1066 | tasksPath, 1067 | taskId, 1068 | 0, 1069 | false, 1070 | '', 1071 | context, 1072 | false 1073 | ); 1074 | 1075 | // Assert - Should complete successfully 1076 | expect(result).toBeDefined(); 1077 | expect(generateTextService).toHaveBeenCalled(); 1078 | }); 1079 | 1080 | test('should use dynamic prompting when numSubtasks is 0', async () => { 1081 | // Mock getPromptManager to return realistic prompt with dynamic content 1082 | const { getPromptManager } = await import( 1083 | '../../../../../scripts/modules/prompt-manager.js' 1084 | ); 1085 | const mockLoadPrompt = jest.fn().mockResolvedValue({ 1086 | systemPrompt: 1087 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into an appropriate number of specific subtasks that can be implemented one by one.', 1088 | userPrompt: 1089 | 'Break down this task into an appropriate number of specific subtasks' 1090 | }); 1091 | getPromptManager.mockReturnValue({ 1092 | loadPrompt: mockLoadPrompt 1093 | }); 1094 | 1095 | // Act 1096 | await expandTask(tasksPath, taskId, 0, false, '', context, false); 1097 | 1098 | // Assert - Verify generateTextService was called 1099 | expect(generateTextService).toHaveBeenCalled(); 1100 | 1101 | // Get the call arguments to verify the system prompt 1102 | const callArgs = generateTextService.mock.calls[0][0]; 1103 | expect(callArgs.systemPrompt).toContain( 1104 | 'an appropriate number of specific subtasks' 1105 | ); 1106 | }); 1107 | 1108 | test('should use specific count prompting when numSubtasks is positive', async () => { 1109 | // Mock getPromptManager to return realistic prompt with specific count 1110 | const { getPromptManager } = await import( 1111 | '../../../../../scripts/modules/prompt-manager.js' 1112 | ); 1113 | const mockLoadPrompt = jest.fn().mockResolvedValue({ 1114 | systemPrompt: 1115 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 5 specific subtasks that can be implemented one by one.', 1116 | userPrompt: 'Break down this task into exactly 5 specific subtasks' 1117 | }); 1118 | getPromptManager.mockReturnValue({ 1119 | loadPrompt: mockLoadPrompt 1120 | }); 1121 | 1122 | // Act 1123 | await expandTask(tasksPath, taskId, 5, false, '', context, false); 1124 | 1125 | // Assert - Verify generateTextService was called 1126 | expect(generateTextService).toHaveBeenCalled(); 1127 | 1128 | // Get the call arguments to verify the system prompt 1129 | const callArgs = generateTextService.mock.calls[0][0]; 1130 | expect(callArgs.systemPrompt).toContain('5 specific subtasks'); 1131 | }); 1132 | 1133 | test('should reject negative numSubtasks values and fallback to default', async () => { 1134 | // Mock getDefaultSubtasks to return a specific value 1135 | getDefaultSubtasks.mockReturnValue(4); 1136 | 1137 | // Mock getPromptManager to return realistic prompt with default count 1138 | const { getPromptManager } = await import( 1139 | '../../../../../scripts/modules/prompt-manager.js' 1140 | ); 1141 | const mockLoadPrompt = jest.fn().mockResolvedValue({ 1142 | systemPrompt: 1143 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 4 specific subtasks that can be implemented one by one.', 1144 | userPrompt: 'Break down this task into exactly 4 specific subtasks' 1145 | }); 1146 | getPromptManager.mockReturnValue({ 1147 | loadPrompt: mockLoadPrompt 1148 | }); 1149 | 1150 | // Act 1151 | await expandTask(tasksPath, taskId, -3, false, '', context, false); 1152 | 1153 | // Assert - Should use default value instead of negative 1154 | expect(generateTextService).toHaveBeenCalled(); 1155 | const callArgs = generateTextService.mock.calls[0][0]; 1156 | expect(callArgs.systemPrompt).toContain('4 specific subtasks'); 1157 | }); 1158 | 1159 | test('should use getDefaultSubtasks when numSubtasks is undefined', async () => { 1160 | // Mock getDefaultSubtasks to return a specific value 1161 | getDefaultSubtasks.mockReturnValue(6); 1162 | 1163 | // Mock getPromptManager to return realistic prompt with default count 1164 | const { getPromptManager } = await import( 1165 | '../../../../../scripts/modules/prompt-manager.js' 1166 | ); 1167 | const mockLoadPrompt = jest.fn().mockResolvedValue({ 1168 | systemPrompt: 1169 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 6 specific subtasks that can be implemented one by one.', 1170 | userPrompt: 'Break down this task into exactly 6 specific subtasks' 1171 | }); 1172 | getPromptManager.mockReturnValue({ 1173 | loadPrompt: mockLoadPrompt 1174 | }); 1175 | 1176 | // Act - Call without specifying numSubtasks (undefined) 1177 | await expandTask(tasksPath, taskId, undefined, false, '', context, false); 1178 | 1179 | // Assert - Should use default value 1180 | expect(generateTextService).toHaveBeenCalled(); 1181 | const callArgs = generateTextService.mock.calls[0][0]; 1182 | expect(callArgs.systemPrompt).toContain('6 specific subtasks'); 1183 | }); 1184 | 1185 | test('should use getDefaultSubtasks when numSubtasks is null', async () => { 1186 | // Mock getDefaultSubtasks to return a specific value 1187 | getDefaultSubtasks.mockReturnValue(7); 1188 | 1189 | // Mock getPromptManager to return realistic prompt with default count 1190 | const { getPromptManager } = await import( 1191 | '../../../../../scripts/modules/prompt-manager.js' 1192 | ); 1193 | const mockLoadPrompt = jest.fn().mockResolvedValue({ 1194 | systemPrompt: 1195 | 'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 7 specific subtasks that can be implemented one by one.', 1196 | userPrompt: 'Break down this task into exactly 7 specific subtasks' 1197 | }); 1198 | getPromptManager.mockReturnValue({ 1199 | loadPrompt: mockLoadPrompt 1200 | }); 1201 | 1202 | // Act - Call with null numSubtasks 1203 | await expandTask(tasksPath, taskId, null, false, '', context, false); 1204 | 1205 | // Assert - Should use default value 1206 | expect(generateTextService).toHaveBeenCalled(); 1207 | const callArgs = generateTextService.mock.calls[0][0]; 1208 | expect(callArgs.systemPrompt).toContain('7 specific subtasks'); 1209 | }); 1210 | }); 1211 | }); 1212 | ``` -------------------------------------------------------------------------------- /scripts/modules/config-manager.js: -------------------------------------------------------------------------------- ```javascript 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import chalk from 'chalk'; 5 | import { z } from 'zod'; 6 | import { AI_COMMAND_NAMES } from '../../src/constants/commands.js'; 7 | import { 8 | LEGACY_CONFIG_FILE, 9 | TASKMASTER_DIR 10 | } from '../../src/constants/paths.js'; 11 | import { 12 | ALL_PROVIDERS, 13 | CUSTOM_PROVIDERS, 14 | CUSTOM_PROVIDERS_ARRAY, 15 | VALIDATED_PROVIDERS 16 | } from '../../src/constants/providers.js'; 17 | import { findConfigPath } from '../../src/utils/path-utils.js'; 18 | import { findProjectRoot, isEmpty, log, resolveEnvVariable } from './utils.js'; 19 | import MODEL_MAP from './supported-models.json' with { type: 'json' }; 20 | 21 | // Calculate __dirname in ESM 22 | const __filename = fileURLToPath(import.meta.url); 23 | const __dirname = path.dirname(__filename); 24 | 25 | // Default configuration values (used if config file is missing or incomplete) 26 | const DEFAULTS = { 27 | models: { 28 | main: { 29 | provider: 'anthropic', 30 | modelId: 'claude-sonnet-4-20250514', 31 | maxTokens: 64000, 32 | temperature: 0.2 33 | }, 34 | research: { 35 | provider: 'perplexity', 36 | modelId: 'sonar', 37 | maxTokens: 8700, 38 | temperature: 0.1 39 | }, 40 | fallback: { 41 | // No default fallback provider/model initially 42 | provider: 'anthropic', 43 | modelId: 'claude-3-7-sonnet-20250219', 44 | maxTokens: 120000, // Default parameters if fallback IS configured 45 | temperature: 0.2 46 | } 47 | }, 48 | global: { 49 | logLevel: 'info', 50 | debug: false, 51 | defaultNumTasks: 10, 52 | defaultSubtasks: 5, 53 | defaultPriority: 'medium', 54 | projectName: 'Task Master', 55 | ollamaBaseURL: 'http://localhost:11434/api', 56 | bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com', 57 | responseLanguage: 'English', 58 | enableCodebaseAnalysis: true 59 | }, 60 | claudeCode: {}, 61 | grokCli: { 62 | timeout: 120000, 63 | workingDirectory: null, 64 | defaultModel: 'grok-4-latest' 65 | } 66 | }; 67 | 68 | // --- Internal Config Loading --- 69 | let loadedConfig = null; 70 | let loadedConfigRoot = null; // Track which root loaded the config 71 | 72 | // Custom Error for configuration issues 73 | class ConfigurationError extends Error { 74 | constructor(message) { 75 | super(message); 76 | this.name = 'ConfigurationError'; 77 | } 78 | } 79 | 80 | function _loadAndValidateConfig(explicitRoot = null) { 81 | const defaults = DEFAULTS; // Use the defined defaults 82 | let rootToUse = explicitRoot; 83 | let configSource = explicitRoot 84 | ? `explicit root (${explicitRoot})` 85 | : 'defaults (no root provided yet)'; 86 | 87 | // ---> If no explicit root, TRY to find it <--- 88 | if (!rootToUse) { 89 | rootToUse = findProjectRoot(); 90 | if (rootToUse) { 91 | configSource = `found root (${rootToUse})`; 92 | } else { 93 | // No root found, use current working directory as fallback 94 | // This prevents infinite loops during initialization 95 | rootToUse = process.cwd(); 96 | configSource = `current directory (${rootToUse}) - no project markers found`; 97 | } 98 | } 99 | // ---> End find project root logic <--- 100 | 101 | // --- Find configuration file --- 102 | let configPath = null; 103 | let config = { ...defaults }; // Start with a deep copy of defaults 104 | let configExists = false; 105 | 106 | // During initialization (no project markers), skip config file search entirely 107 | const hasProjectMarkers = 108 | fs.existsSync(path.join(rootToUse, TASKMASTER_DIR)) || 109 | fs.existsSync(path.join(rootToUse, LEGACY_CONFIG_FILE)); 110 | 111 | if (hasProjectMarkers) { 112 | // Only try to find config if we have project markers 113 | // This prevents the repeated warnings during init 114 | configPath = findConfigPath(null, { projectRoot: rootToUse }); 115 | } 116 | 117 | if (configPath) { 118 | configExists = true; 119 | const isLegacy = configPath.endsWith(LEGACY_CONFIG_FILE); 120 | 121 | try { 122 | const rawData = fs.readFileSync(configPath, 'utf-8'); 123 | const parsedConfig = JSON.parse(rawData); 124 | 125 | // Deep merge parsed config onto defaults 126 | config = { 127 | models: { 128 | main: { ...defaults.models.main, ...parsedConfig?.models?.main }, 129 | research: { 130 | ...defaults.models.research, 131 | ...parsedConfig?.models?.research 132 | }, 133 | fallback: 134 | parsedConfig?.models?.fallback?.provider && 135 | parsedConfig?.models?.fallback?.modelId 136 | ? { ...defaults.models.fallback, ...parsedConfig.models.fallback } 137 | : { ...defaults.models.fallback } 138 | }, 139 | global: { ...defaults.global, ...parsedConfig?.global }, 140 | claudeCode: { ...defaults.claudeCode, ...parsedConfig?.claudeCode }, 141 | grokCli: { ...defaults.grokCli, ...parsedConfig?.grokCli } 142 | }; 143 | configSource = `file (${configPath})`; // Update source info 144 | 145 | // Issue deprecation warning if using legacy config file 146 | if (isLegacy) { 147 | console.warn( 148 | chalk.yellow( 149 | `⚠️ DEPRECATION WARNING: Found configuration in legacy location '${configPath}'. Please migrate to .taskmaster/config.json. Run 'task-master migrate' to automatically migrate your project.` 150 | ) 151 | ); 152 | } 153 | 154 | // --- Validation (Warn if file content is invalid) --- 155 | // Use log.warn for consistency 156 | if (!validateProvider(config.models.main.provider)) { 157 | console.warn( 158 | chalk.yellow( 159 | `Warning: Invalid main provider "${config.models.main.provider}" in ${configPath}. Falling back to default.` 160 | ) 161 | ); 162 | config.models.main = { ...defaults.models.main }; 163 | } 164 | if (!validateProvider(config.models.research.provider)) { 165 | console.warn( 166 | chalk.yellow( 167 | `Warning: Invalid research provider "${config.models.research.provider}" in ${configPath}. Falling back to default.` 168 | ) 169 | ); 170 | config.models.research = { ...defaults.models.research }; 171 | } 172 | if ( 173 | config.models.fallback?.provider && 174 | !validateProvider(config.models.fallback.provider) 175 | ) { 176 | console.warn( 177 | chalk.yellow( 178 | `Warning: Invalid fallback provider "${config.models.fallback.provider}" in ${configPath}. Fallback model configuration will be ignored.` 179 | ) 180 | ); 181 | config.models.fallback.provider = undefined; 182 | config.models.fallback.modelId = undefined; 183 | } 184 | if (config.claudeCode && !isEmpty(config.claudeCode)) { 185 | config.claudeCode = validateClaudeCodeSettings(config.claudeCode); 186 | } 187 | } catch (error) { 188 | // Use console.error for actual errors during parsing 189 | console.error( 190 | chalk.red( 191 | `Error reading or parsing ${configPath}: ${error.message}. Using default configuration.` 192 | ) 193 | ); 194 | config = { ...defaults }; // Reset to defaults on parse error 195 | configSource = `defaults (parse error at ${configPath})`; 196 | } 197 | } else { 198 | // Config file doesn't exist at the determined rootToUse. 199 | if (explicitRoot) { 200 | // Only warn if an explicit root was *expected*. 201 | console.warn( 202 | chalk.yellow( 203 | `Warning: Configuration file not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.` 204 | ) 205 | ); 206 | } else { 207 | // Don't warn about missing config during initialization 208 | // Only warn if this looks like an existing project (has .taskmaster dir or legacy config marker) 209 | const hasTaskmasterDir = fs.existsSync( 210 | path.join(rootToUse, TASKMASTER_DIR) 211 | ); 212 | const hasLegacyMarker = fs.existsSync( 213 | path.join(rootToUse, LEGACY_CONFIG_FILE) 214 | ); 215 | 216 | if (hasTaskmasterDir || hasLegacyMarker) { 217 | console.warn( 218 | chalk.yellow( 219 | `Warning: Configuration file not found at derived root (${rootToUse}). Using defaults.` 220 | ) 221 | ); 222 | } 223 | } 224 | // Keep config as defaults 225 | config = { ...defaults }; 226 | configSource = `defaults (no config file found at ${rootToUse})`; 227 | } 228 | 229 | return config; 230 | } 231 | 232 | /** 233 | * Gets the current configuration, loading it if necessary. 234 | * Handles MCP initialization context gracefully. 235 | * @param {string|null} explicitRoot - Optional explicit path to the project root. 236 | * @param {boolean} forceReload - Force reloading the config file. 237 | * @returns {object} The loaded configuration object. 238 | */ 239 | function getConfig(explicitRoot = null, forceReload = false) { 240 | // Determine if a reload is necessary 241 | const needsLoad = 242 | !loadedConfig || 243 | forceReload || 244 | (explicitRoot && explicitRoot !== loadedConfigRoot); 245 | 246 | if (needsLoad) { 247 | const newConfig = _loadAndValidateConfig(explicitRoot); // _load handles null explicitRoot 248 | 249 | // Only update the global cache if loading was forced or if an explicit root 250 | // was provided (meaning we attempted to load a specific project's config). 251 | // We avoid caching the initial default load triggered without an explicitRoot. 252 | if (forceReload || explicitRoot) { 253 | loadedConfig = newConfig; 254 | loadedConfigRoot = explicitRoot; // Store the root used for this loaded config 255 | } 256 | return newConfig; // Return the newly loaded/default config 257 | } 258 | 259 | // If no load was needed, return the cached config 260 | return loadedConfig; 261 | } 262 | 263 | /** 264 | * Validates if a provider name is supported. 265 | * Custom providers (azure, vertex, bedrock, openrouter, ollama) are always allowed. 266 | * Validated providers must exist in the MODEL_MAP from supported-models.json. 267 | * @param {string} providerName The name of the provider. 268 | * @returns {boolean} True if the provider is valid, false otherwise. 269 | */ 270 | function validateProvider(providerName) { 271 | // Custom providers are always allowed 272 | if (CUSTOM_PROVIDERS_ARRAY.includes(providerName)) { 273 | return true; 274 | } 275 | 276 | // Validated providers must exist in MODEL_MAP 277 | if (VALIDATED_PROVIDERS.includes(providerName)) { 278 | return !!(MODEL_MAP && MODEL_MAP[providerName]); 279 | } 280 | 281 | // Unknown providers are not allowed 282 | return false; 283 | } 284 | 285 | /** 286 | * Optional: Validates if a modelId is known for a given provider based on MODEL_MAP. 287 | * This is a non-strict validation; an unknown model might still be valid. 288 | * @param {string} providerName The name of the provider. 289 | * @param {string} modelId The model ID. 290 | * @returns {boolean} True if the modelId is in the map for the provider, false otherwise. 291 | */ 292 | function validateProviderModelCombination(providerName, modelId) { 293 | // If provider isn't even in our map, we can't validate the model 294 | if (!MODEL_MAP[providerName]) { 295 | return true; // Allow unknown providers or those without specific model lists 296 | } 297 | // If the provider is known, check if the model is in its list OR if the list is empty (meaning accept any) 298 | return ( 299 | MODEL_MAP[providerName].length === 0 || 300 | // Use .some() to check the 'id' property of objects in the array 301 | MODEL_MAP[providerName].some((modelObj) => modelObj.id === modelId) 302 | ); 303 | } 304 | 305 | /** 306 | * Validates Claude Code AI provider custom settings 307 | * @param {object} settings The settings to validate 308 | * @returns {object} The validated settings 309 | */ 310 | function validateClaudeCodeSettings(settings) { 311 | // Define the base settings schema without commandSpecific first 312 | const BaseSettingsSchema = z.object({ 313 | maxTurns: z.number().int().positive().optional(), 314 | customSystemPrompt: z.string().optional(), 315 | appendSystemPrompt: z.string().optional(), 316 | permissionMode: z 317 | .enum(['default', 'acceptEdits', 'plan', 'bypassPermissions']) 318 | .optional(), 319 | allowedTools: z.array(z.string()).optional(), 320 | disallowedTools: z.array(z.string()).optional(), 321 | mcpServers: z 322 | .record( 323 | z.string(), 324 | z.object({ 325 | type: z.enum(['stdio', 'sse']).optional(), 326 | command: z.string(), 327 | args: z.array(z.string()).optional(), 328 | env: z.record(z.string()).optional(), 329 | url: z.string().url().optional(), 330 | headers: z.record(z.string()).optional() 331 | }) 332 | ) 333 | .optional() 334 | }); 335 | 336 | // Define CommandSpecificSchema using the base schema 337 | const CommandSpecificSchema = z.record( 338 | z.enum(AI_COMMAND_NAMES), 339 | BaseSettingsSchema 340 | ); 341 | 342 | // Define the full settings schema with commandSpecific 343 | const SettingsSchema = BaseSettingsSchema.extend({ 344 | commandSpecific: CommandSpecificSchema.optional() 345 | }); 346 | 347 | let validatedSettings = {}; 348 | 349 | try { 350 | validatedSettings = SettingsSchema.parse(settings); 351 | } catch (error) { 352 | console.warn( 353 | chalk.yellow( 354 | `Warning: Invalid Claude Code settings in config: ${error.message}. Falling back to default.` 355 | ) 356 | ); 357 | 358 | validatedSettings = {}; 359 | } 360 | 361 | return validatedSettings; 362 | } 363 | 364 | // --- Claude Code Settings Getters --- 365 | 366 | function getClaudeCodeSettings(explicitRoot = null, forceReload = false) { 367 | const config = getConfig(explicitRoot, forceReload); 368 | // Ensure Claude Code defaults are applied if Claude Code section is missing 369 | return { ...DEFAULTS.claudeCode, ...(config?.claudeCode || {}) }; 370 | } 371 | 372 | function getClaudeCodeSettingsForCommand( 373 | commandName, 374 | explicitRoot = null, 375 | forceReload = false 376 | ) { 377 | const settings = getClaudeCodeSettings(explicitRoot, forceReload); 378 | const commandSpecific = settings?.commandSpecific || {}; 379 | return { ...settings, ...commandSpecific[commandName] }; 380 | } 381 | 382 | function getGrokCliSettings(explicitRoot = null, forceReload = false) { 383 | const config = getConfig(explicitRoot, forceReload); 384 | // Ensure Grok CLI defaults are applied if Grok CLI section is missing 385 | return { ...DEFAULTS.grokCli, ...(config?.grokCli || {}) }; 386 | } 387 | 388 | function getGrokCliSettingsForCommand( 389 | commandName, 390 | explicitRoot = null, 391 | forceReload = false 392 | ) { 393 | const settings = getGrokCliSettings(explicitRoot, forceReload); 394 | const commandSpecific = settings?.commandSpecific || {}; 395 | return { ...settings, ...commandSpecific[commandName] }; 396 | } 397 | 398 | // --- Role-Specific Getters --- 399 | 400 | function getModelConfigForRole(role, explicitRoot = null) { 401 | const config = getConfig(explicitRoot); 402 | const roleConfig = config?.models?.[role]; 403 | if (!roleConfig) { 404 | log( 405 | 'warn', 406 | `No model configuration found for role: ${role}. Returning default.` 407 | ); 408 | return DEFAULTS.models[role] || {}; 409 | } 410 | return roleConfig; 411 | } 412 | 413 | function getMainProvider(explicitRoot = null) { 414 | return getModelConfigForRole('main', explicitRoot).provider; 415 | } 416 | 417 | function getMainModelId(explicitRoot = null) { 418 | return getModelConfigForRole('main', explicitRoot).modelId; 419 | } 420 | 421 | function getMainMaxTokens(explicitRoot = null) { 422 | // Directly return value from config (which includes defaults) 423 | return getModelConfigForRole('main', explicitRoot).maxTokens; 424 | } 425 | 426 | function getMainTemperature(explicitRoot = null) { 427 | // Directly return value from config 428 | return getModelConfigForRole('main', explicitRoot).temperature; 429 | } 430 | 431 | function getResearchProvider(explicitRoot = null) { 432 | return getModelConfigForRole('research', explicitRoot).provider; 433 | } 434 | 435 | /** 436 | * Check if codebase analysis feature flag is enabled across all sources 437 | * Priority: .env > MCP env > config.json 438 | * @param {object|null} session - MCP session object (optional) 439 | * @param {string|null} projectRoot - Project root path (optional) 440 | * @returns {boolean} True if codebase analysis is enabled 441 | */ 442 | function isCodebaseAnalysisEnabled(session = null, projectRoot = null) { 443 | // Priority 1: Environment variable 444 | const envFlag = resolveEnvVariable( 445 | 'TASKMASTER_ENABLE_CODEBASE_ANALYSIS', 446 | session, 447 | projectRoot 448 | ); 449 | if (envFlag !== null && envFlag !== undefined && envFlag !== '') { 450 | return envFlag.toLowerCase() === 'true' || envFlag === '1'; 451 | } 452 | 453 | // Priority 2: MCP session environment 454 | if (session?.env?.TASKMASTER_ENABLE_CODEBASE_ANALYSIS) { 455 | const mcpFlag = session.env.TASKMASTER_ENABLE_CODEBASE_ANALYSIS; 456 | return mcpFlag.toLowerCase() === 'true' || mcpFlag === '1'; 457 | } 458 | 459 | // Priority 3: Configuration file 460 | const globalConfig = getGlobalConfig(projectRoot); 461 | return globalConfig.enableCodebaseAnalysis !== false; // Default to true 462 | } 463 | 464 | /** 465 | * Check if codebase analysis is available and enabled 466 | * @param {boolean} useResearch - Whether to check research provider or main provider 467 | * @param {string|null} projectRoot - Project root path (optional) 468 | * @param {object|null} session - MCP session object (optional) 469 | * @returns {boolean} True if codebase analysis is available and enabled 470 | */ 471 | function hasCodebaseAnalysis( 472 | useResearch = false, 473 | projectRoot = null, 474 | session = null 475 | ) { 476 | // First check if the feature is enabled 477 | if (!isCodebaseAnalysisEnabled(session, projectRoot)) { 478 | return false; 479 | } 480 | 481 | // Then check if a codebase analysis provider is configured 482 | const currentProvider = useResearch 483 | ? getResearchProvider(projectRoot) 484 | : getMainProvider(projectRoot); 485 | 486 | return ( 487 | currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE || 488 | currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI || 489 | currentProvider === CUSTOM_PROVIDERS.GROK_CLI 490 | ); 491 | } 492 | 493 | function getResearchModelId(explicitRoot = null) { 494 | return getModelConfigForRole('research', explicitRoot).modelId; 495 | } 496 | 497 | function getResearchMaxTokens(explicitRoot = null) { 498 | // Directly return value from config 499 | return getModelConfigForRole('research', explicitRoot).maxTokens; 500 | } 501 | 502 | function getResearchTemperature(explicitRoot = null) { 503 | // Directly return value from config 504 | return getModelConfigForRole('research', explicitRoot).temperature; 505 | } 506 | 507 | function getFallbackProvider(explicitRoot = null) { 508 | // Directly return value from config (will be undefined if not set) 509 | return getModelConfigForRole('fallback', explicitRoot).provider; 510 | } 511 | 512 | function getFallbackModelId(explicitRoot = null) { 513 | // Directly return value from config 514 | return getModelConfigForRole('fallback', explicitRoot).modelId; 515 | } 516 | 517 | function getFallbackMaxTokens(explicitRoot = null) { 518 | // Directly return value from config 519 | return getModelConfigForRole('fallback', explicitRoot).maxTokens; 520 | } 521 | 522 | function getFallbackTemperature(explicitRoot = null) { 523 | // Directly return value from config 524 | return getModelConfigForRole('fallback', explicitRoot).temperature; 525 | } 526 | 527 | // --- Global Settings Getters --- 528 | 529 | function getGlobalConfig(explicitRoot = null) { 530 | const config = getConfig(explicitRoot); 531 | // Ensure global defaults are applied if global section is missing 532 | return { ...DEFAULTS.global, ...(config?.global || {}) }; 533 | } 534 | 535 | function getLogLevel(explicitRoot = null) { 536 | // Directly return value from config 537 | return getGlobalConfig(explicitRoot).logLevel.toLowerCase(); 538 | } 539 | 540 | function getDebugFlag(explicitRoot = null) { 541 | // Directly return value from config, ensure boolean 542 | return getGlobalConfig(explicitRoot).debug === true; 543 | } 544 | 545 | function getDefaultSubtasks(explicitRoot = null) { 546 | // Directly return value from config, ensure integer 547 | const val = getGlobalConfig(explicitRoot).defaultSubtasks; 548 | const parsedVal = parseInt(val, 10); 549 | return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultSubtasks : parsedVal; 550 | } 551 | 552 | function getDefaultNumTasks(explicitRoot = null) { 553 | const val = getGlobalConfig(explicitRoot).defaultNumTasks; 554 | const parsedVal = parseInt(val, 10); 555 | return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultNumTasks : parsedVal; 556 | } 557 | 558 | function getDefaultPriority(explicitRoot = null) { 559 | // Directly return value from config 560 | return getGlobalConfig(explicitRoot).defaultPriority; 561 | } 562 | 563 | function getProjectName(explicitRoot = null) { 564 | // Directly return value from config 565 | return getGlobalConfig(explicitRoot).projectName; 566 | } 567 | 568 | function getOllamaBaseURL(explicitRoot = null) { 569 | // Directly return value from config 570 | return getGlobalConfig(explicitRoot).ollamaBaseURL; 571 | } 572 | 573 | function getAzureBaseURL(explicitRoot = null) { 574 | // Directly return value from config 575 | return getGlobalConfig(explicitRoot).azureBaseURL; 576 | } 577 | 578 | function getBedrockBaseURL(explicitRoot = null) { 579 | // Directly return value from config 580 | return getGlobalConfig(explicitRoot).bedrockBaseURL; 581 | } 582 | 583 | /** 584 | * Gets the Google Cloud project ID for Vertex AI from configuration 585 | * @param {string|null} explicitRoot - Optional explicit path to the project root. 586 | * @returns {string|null} The project ID or null if not configured 587 | */ 588 | function getVertexProjectId(explicitRoot = null) { 589 | // Return value from config 590 | return getGlobalConfig(explicitRoot).vertexProjectId; 591 | } 592 | 593 | /** 594 | * Gets the Google Cloud location for Vertex AI from configuration 595 | * @param {string|null} explicitRoot - Optional explicit path to the project root. 596 | * @returns {string} The location or default value of "us-central1" 597 | */ 598 | function getVertexLocation(explicitRoot = null) { 599 | // Return value from config or default 600 | return getGlobalConfig(explicitRoot).vertexLocation || 'us-central1'; 601 | } 602 | 603 | function getResponseLanguage(explicitRoot = null) { 604 | // Directly return value from config 605 | return getGlobalConfig(explicitRoot).responseLanguage; 606 | } 607 | 608 | function getCodebaseAnalysisEnabled(explicitRoot = null) { 609 | // Return boolean-safe value with default true 610 | return getGlobalConfig(explicitRoot).enableCodebaseAnalysis !== false; 611 | } 612 | 613 | /** 614 | * Gets model parameters (maxTokens, temperature) for a specific role, 615 | * considering model-specific overrides from supported-models.json. 616 | * @param {string} role - The role ('main', 'research', 'fallback'). 617 | * @param {string|null} explicitRoot - Optional explicit path to the project root. 618 | * @returns {{maxTokens: number, temperature: number}} 619 | */ 620 | function getParametersForRole(role, explicitRoot = null) { 621 | const roleConfig = getModelConfigForRole(role, explicitRoot); 622 | const roleMaxTokens = roleConfig.maxTokens; 623 | const roleTemperature = roleConfig.temperature; 624 | const modelId = roleConfig.modelId; 625 | const providerName = roleConfig.provider; 626 | 627 | let effectiveMaxTokens = roleMaxTokens; // Start with the role's default 628 | let effectiveTemperature = roleTemperature; // Start with the role's default 629 | 630 | try { 631 | // Find the model definition in MODEL_MAP 632 | const providerModels = MODEL_MAP[providerName]; 633 | if (providerModels && Array.isArray(providerModels)) { 634 | const modelDefinition = providerModels.find((m) => m.id === modelId); 635 | 636 | // Check if a model-specific max_tokens is defined and valid 637 | if ( 638 | modelDefinition && 639 | typeof modelDefinition.max_tokens === 'number' && 640 | modelDefinition.max_tokens > 0 641 | ) { 642 | const modelSpecificMaxTokens = modelDefinition.max_tokens; 643 | // Use the minimum of the role default and the model specific limit 644 | effectiveMaxTokens = Math.min(roleMaxTokens, modelSpecificMaxTokens); 645 | log( 646 | 'debug', 647 | `Applying model-specific max_tokens (${modelSpecificMaxTokens}) for ${modelId}. Effective limit: ${effectiveMaxTokens}` 648 | ); 649 | } else { 650 | log( 651 | 'debug', 652 | `No valid model-specific max_tokens override found for ${modelId}. Using role default: ${roleMaxTokens}` 653 | ); 654 | } 655 | 656 | // Check if a model-specific temperature is defined 657 | if ( 658 | modelDefinition && 659 | typeof modelDefinition.temperature === 'number' && 660 | modelDefinition.temperature >= 0 && 661 | modelDefinition.temperature <= 1 662 | ) { 663 | effectiveTemperature = modelDefinition.temperature; 664 | log( 665 | 'debug', 666 | `Applying model-specific temperature (${modelDefinition.temperature}) for ${modelId}` 667 | ); 668 | } 669 | } else { 670 | // Special handling for custom OpenRouter models 671 | if (providerName === CUSTOM_PROVIDERS.OPENROUTER) { 672 | // Use a conservative default for OpenRouter models not in our list 673 | const openrouterDefault = 32768; 674 | effectiveMaxTokens = Math.min(roleMaxTokens, openrouterDefault); 675 | log( 676 | 'debug', 677 | `Custom OpenRouter model ${modelId} detected. Using conservative max_tokens: ${effectiveMaxTokens}` 678 | ); 679 | } else { 680 | log( 681 | 'debug', 682 | `No model definitions found for provider ${providerName} in MODEL_MAP. Using role default maxTokens: ${roleMaxTokens}` 683 | ); 684 | } 685 | } 686 | } catch (lookupError) { 687 | log( 688 | 'warn', 689 | `Error looking up model-specific parameters for ${modelId}: ${lookupError.message}. Using role defaults.` 690 | ); 691 | // Fallback to role defaults on error 692 | effectiveMaxTokens = roleMaxTokens; 693 | effectiveTemperature = roleTemperature; 694 | } 695 | 696 | return { 697 | maxTokens: effectiveMaxTokens, 698 | temperature: effectiveTemperature 699 | }; 700 | } 701 | 702 | /** 703 | * Checks if the API key for a given provider is set in the environment. 704 | * Checks process.env first, then session.env if session is provided, then .env file if projectRoot provided. 705 | * @param {string} providerName - The name of the provider (e.g., 'openai', 'anthropic'). 706 | * @param {object|null} [session=null] - The MCP session object (optional). 707 | * @param {string|null} [projectRoot=null] - The project root directory (optional, for .env file check). 708 | * @returns {boolean} True if the API key is set, false otherwise. 709 | */ 710 | function isApiKeySet(providerName, session = null, projectRoot = null) { 711 | // Define the expected environment variable name for each provider 712 | 713 | // Providers that don't require API keys for authentication 714 | const providersWithoutApiKeys = [ 715 | CUSTOM_PROVIDERS.OLLAMA, 716 | CUSTOM_PROVIDERS.BEDROCK, 717 | CUSTOM_PROVIDERS.MCP, 718 | CUSTOM_PROVIDERS.GEMINI_CLI, 719 | CUSTOM_PROVIDERS.GROK_CLI 720 | ]; 721 | 722 | if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) { 723 | return true; // Indicate key status is effectively "OK" 724 | } 725 | 726 | // Claude Code doesn't require an API key 727 | if (providerName?.toLowerCase() === 'claude-code') { 728 | return true; // No API key needed 729 | } 730 | 731 | const keyMap = { 732 | openai: 'OPENAI_API_KEY', 733 | anthropic: 'ANTHROPIC_API_KEY', 734 | google: 'GOOGLE_API_KEY', 735 | perplexity: 'PERPLEXITY_API_KEY', 736 | mistral: 'MISTRAL_API_KEY', 737 | azure: 'AZURE_OPENAI_API_KEY', 738 | openrouter: 'OPENROUTER_API_KEY', 739 | xai: 'XAI_API_KEY', 740 | groq: 'GROQ_API_KEY', 741 | vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google 742 | 'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency 743 | bedrock: 'AWS_ACCESS_KEY_ID' // Bedrock uses AWS credentials 744 | // Add other providers as needed 745 | }; 746 | 747 | const providerKey = providerName?.toLowerCase(); 748 | if (!providerKey || !keyMap[providerKey]) { 749 | log('warn', `Unknown provider name: ${providerName} in isApiKeySet check.`); 750 | return false; 751 | } 752 | 753 | const envVarName = keyMap[providerKey]; 754 | const apiKeyValue = resolveEnvVariable(envVarName, session, projectRoot); 755 | 756 | // Check if the key exists, is not empty, and is not a placeholder 757 | return ( 758 | apiKeyValue && 759 | apiKeyValue.trim() !== '' && 760 | !/YOUR_.*_API_KEY_HERE/.test(apiKeyValue) && // General placeholder check 761 | !apiKeyValue.includes('KEY_HERE') 762 | ); // Another common placeholder pattern 763 | } 764 | 765 | /** 766 | * Checks the API key status within .cursor/mcp.json for a given provider. 767 | * Reads the mcp.json file, finds the taskmaster-ai server config, and checks the relevant env var. 768 | * @param {string} providerName The name of the provider. 769 | * @param {string|null} projectRoot - Optional explicit path to the project root. 770 | * @returns {boolean} True if the key exists and is not a placeholder, false otherwise. 771 | */ 772 | function getMcpApiKeyStatus(providerName, projectRoot = null) { 773 | const rootDir = projectRoot || findProjectRoot(); // Use existing root finding 774 | if (!rootDir) { 775 | console.warn( 776 | chalk.yellow('Warning: Could not find project root to check mcp.json.') 777 | ); 778 | return false; // Cannot check without root 779 | } 780 | const mcpConfigPath = path.join(rootDir, '.cursor', 'mcp.json'); 781 | 782 | if (!fs.existsSync(mcpConfigPath)) { 783 | // console.warn(chalk.yellow('Warning: .cursor/mcp.json not found.')); 784 | return false; // File doesn't exist 785 | } 786 | 787 | try { 788 | const mcpConfigRaw = fs.readFileSync(mcpConfigPath, 'utf-8'); 789 | const mcpConfig = JSON.parse(mcpConfigRaw); 790 | 791 | const mcpEnv = 792 | mcpConfig?.mcpServers?.['task-master-ai']?.env || 793 | mcpConfig?.mcpServers?.['taskmaster-ai']?.env; 794 | if (!mcpEnv) { 795 | return false; 796 | } 797 | 798 | let apiKeyToCheck = null; 799 | let placeholderValue = null; 800 | 801 | switch (providerName) { 802 | case 'anthropic': 803 | apiKeyToCheck = mcpEnv.ANTHROPIC_API_KEY; 804 | placeholderValue = 'YOUR_ANTHROPIC_API_KEY_HERE'; 805 | break; 806 | case 'openai': 807 | apiKeyToCheck = mcpEnv.OPENAI_API_KEY; 808 | placeholderValue = 'YOUR_OPENAI_API_KEY_HERE'; // Assuming placeholder matches OPENAI 809 | break; 810 | case 'openrouter': 811 | apiKeyToCheck = mcpEnv.OPENROUTER_API_KEY; 812 | placeholderValue = 'YOUR_OPENROUTER_API_KEY_HERE'; 813 | break; 814 | case 'google': 815 | apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; 816 | placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE'; 817 | break; 818 | case 'perplexity': 819 | apiKeyToCheck = mcpEnv.PERPLEXITY_API_KEY; 820 | placeholderValue = 'YOUR_PERPLEXITY_API_KEY_HERE'; 821 | break; 822 | case 'xai': 823 | apiKeyToCheck = mcpEnv.XAI_API_KEY; 824 | placeholderValue = 'YOUR_XAI_API_KEY_HERE'; 825 | break; 826 | case 'groq': 827 | apiKeyToCheck = mcpEnv.GROQ_API_KEY; 828 | placeholderValue = 'YOUR_GROQ_API_KEY_HERE'; 829 | break; 830 | case 'ollama': 831 | return true; // No key needed 832 | case 'claude-code': 833 | return true; // No key needed 834 | case 'mistral': 835 | apiKeyToCheck = mcpEnv.MISTRAL_API_KEY; 836 | placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE'; 837 | break; 838 | case 'azure': 839 | apiKeyToCheck = mcpEnv.AZURE_OPENAI_API_KEY; 840 | placeholderValue = 'YOUR_AZURE_OPENAI_API_KEY_HERE'; 841 | break; 842 | case 'vertex': 843 | apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; // Vertex uses Google API key 844 | placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE'; 845 | break; 846 | case 'bedrock': 847 | apiKeyToCheck = mcpEnv.AWS_ACCESS_KEY_ID; // Bedrock uses AWS credentials 848 | placeholderValue = 'YOUR_AWS_ACCESS_KEY_ID_HERE'; 849 | break; 850 | default: 851 | return false; // Unknown provider 852 | } 853 | 854 | return !!apiKeyToCheck && !/KEY_HERE$/.test(apiKeyToCheck); 855 | } catch (error) { 856 | console.error( 857 | chalk.red(`Error reading or parsing .cursor/mcp.json: ${error.message}`) 858 | ); 859 | return false; 860 | } 861 | } 862 | 863 | /** 864 | * Gets a list of available models based on the MODEL_MAP. 865 | * @returns {Array<{id: string, name: string, provider: string, swe_score: number|null, cost_per_1m_tokens: {input: number|null, output: number|null}|null, allowed_roles: string[]}>} 866 | */ 867 | function getAvailableModels() { 868 | const available = []; 869 | for (const [provider, models] of Object.entries(MODEL_MAP)) { 870 | if (models.length > 0) { 871 | models 872 | .filter((modelObj) => Boolean(modelObj.supported)) 873 | .forEach((modelObj) => { 874 | // Basic name generation - can be improved 875 | const modelId = modelObj.id; 876 | const sweScore = modelObj.swe_score; 877 | const cost = modelObj.cost_per_1m_tokens; 878 | const allowedRoles = modelObj.allowed_roles || ['main', 'fallback']; 879 | const nameParts = modelId 880 | .split('-') 881 | .map((p) => p.charAt(0).toUpperCase() + p.slice(1)); 882 | // Handle specific known names better if needed 883 | let name = nameParts.join(' '); 884 | if (modelId === 'claude-3.5-sonnet-20240620') 885 | name = 'Claude 3.5 Sonnet'; 886 | if (modelId === 'claude-3-7-sonnet-20250219') 887 | name = 'Claude 3.7 Sonnet'; 888 | if (modelId === 'gpt-4o') name = 'GPT-4o'; 889 | if (modelId === 'gpt-4-turbo') name = 'GPT-4 Turbo'; 890 | if (modelId === 'sonar-pro') name = 'Perplexity Sonar Pro'; 891 | if (modelId === 'sonar-mini') name = 'Perplexity Sonar Mini'; 892 | 893 | available.push({ 894 | id: modelId, 895 | name: name, 896 | provider: provider, 897 | swe_score: sweScore, 898 | cost_per_1m_tokens: cost, 899 | allowed_roles: allowedRoles, 900 | max_tokens: modelObj.max_tokens 901 | }); 902 | }); 903 | } else { 904 | // For providers with empty lists (like ollama), maybe add a placeholder or skip 905 | available.push({ 906 | id: `[${provider}-any]`, 907 | name: `Any (${provider})`, 908 | provider: provider 909 | }); 910 | } 911 | } 912 | return available; 913 | } 914 | 915 | /** 916 | * Writes the configuration object to the file. 917 | * @param {Object} config The configuration object to write. 918 | * @param {string|null} explicitRoot - Optional explicit path to the project root. 919 | * @returns {boolean} True if successful, false otherwise. 920 | */ 921 | function writeConfig(config, explicitRoot = null) { 922 | // ---> Determine root path reliably <--- 923 | let rootPath = explicitRoot; 924 | if (explicitRoot === null || explicitRoot === undefined) { 925 | // Logic matching _loadAndValidateConfig 926 | const foundRoot = findProjectRoot(); // *** Explicitly call findProjectRoot *** 927 | if (!foundRoot) { 928 | console.error( 929 | chalk.red( 930 | 'Error: Could not determine project root. Configuration not saved.' 931 | ) 932 | ); 933 | return false; 934 | } 935 | rootPath = foundRoot; 936 | } 937 | // ---> End determine root path logic <--- 938 | 939 | // Use new config location: .taskmaster/config.json 940 | const taskmasterDir = path.join(rootPath, '.taskmaster'); 941 | const configPath = path.join(taskmasterDir, 'config.json'); 942 | 943 | try { 944 | // Ensure .taskmaster directory exists 945 | if (!fs.existsSync(taskmasterDir)) { 946 | fs.mkdirSync(taskmasterDir, { recursive: true }); 947 | } 948 | 949 | fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); 950 | loadedConfig = config; // Update the cache after successful write 951 | return true; 952 | } catch (error) { 953 | console.error( 954 | chalk.red( 955 | `Error writing configuration to ${configPath}: ${error.message}` 956 | ) 957 | ); 958 | return false; 959 | } 960 | } 961 | 962 | /** 963 | * Checks if a configuration file exists at the project root (new or legacy location) 964 | * @param {string|null} explicitRoot - Optional explicit path to the project root 965 | * @returns {boolean} True if the file exists, false otherwise 966 | */ 967 | function isConfigFilePresent(explicitRoot = null) { 968 | return findConfigPath(null, { projectRoot: explicitRoot }) !== null; 969 | } 970 | 971 | /** 972 | * Gets the user ID from the configuration. 973 | * @param {string|null} explicitRoot - Optional explicit path to the project root. 974 | * @returns {string|null} The user ID or null if not found. 975 | */ 976 | function getUserId(explicitRoot = null) { 977 | const config = getConfig(explicitRoot); 978 | if (!config.global) { 979 | config.global = {}; // Ensure global object exists 980 | } 981 | if (!config.global.userId) { 982 | config.global.userId = '1234567890'; 983 | // Attempt to write the updated config. 984 | // It's important that writeConfig correctly resolves the path 985 | // using explicitRoot, similar to how getConfig does. 986 | const success = writeConfig(config, explicitRoot); 987 | if (!success) { 988 | // Log an error or handle the failure to write, 989 | // though for now, we'll proceed with the in-memory default. 990 | log( 991 | 'warning', 992 | 'Failed to write updated configuration with new userId. Please let the developers know.' 993 | ); 994 | } 995 | } 996 | return config.global.userId; 997 | } 998 | 999 | /** 1000 | * Gets a list of all known provider names (both validated and custom). 1001 | * @returns {string[]} An array of all provider names. 1002 | */ 1003 | function getAllProviders() { 1004 | return ALL_PROVIDERS; 1005 | } 1006 | 1007 | function getBaseUrlForRole(role, explicitRoot = null) { 1008 | const roleConfig = getModelConfigForRole(role, explicitRoot); 1009 | if (roleConfig && typeof roleConfig.baseURL === 'string') { 1010 | return roleConfig.baseURL; 1011 | } 1012 | const provider = roleConfig?.provider; 1013 | if (provider) { 1014 | const envVarName = `${provider.toUpperCase()}_BASE_URL`; 1015 | return resolveEnvVariable(envVarName, null, explicitRoot); 1016 | } 1017 | return undefined; 1018 | } 1019 | 1020 | // Export the providers without API keys array for use in other modules 1021 | export const providersWithoutApiKeys = [ 1022 | CUSTOM_PROVIDERS.OLLAMA, 1023 | CUSTOM_PROVIDERS.BEDROCK, 1024 | CUSTOM_PROVIDERS.GEMINI_CLI, 1025 | CUSTOM_PROVIDERS.GROK_CLI, 1026 | CUSTOM_PROVIDERS.MCP 1027 | ]; 1028 | 1029 | export { 1030 | // Core config access 1031 | getConfig, 1032 | writeConfig, 1033 | ConfigurationError, 1034 | isConfigFilePresent, 1035 | // Claude Code settings 1036 | getClaudeCodeSettings, 1037 | getClaudeCodeSettingsForCommand, 1038 | // Grok CLI settings 1039 | getGrokCliSettings, 1040 | getGrokCliSettingsForCommand, 1041 | // Validation 1042 | validateProvider, 1043 | validateProviderModelCombination, 1044 | validateClaudeCodeSettings, 1045 | VALIDATED_PROVIDERS, 1046 | CUSTOM_PROVIDERS, 1047 | ALL_PROVIDERS, 1048 | MODEL_MAP, 1049 | getAvailableModels, 1050 | // Role-specific getters (No env var overrides) 1051 | getMainProvider, 1052 | getMainModelId, 1053 | getMainMaxTokens, 1054 | getMainTemperature, 1055 | getResearchProvider, 1056 | getResearchModelId, 1057 | getResearchMaxTokens, 1058 | getResearchTemperature, 1059 | hasCodebaseAnalysis, 1060 | getFallbackProvider, 1061 | getFallbackModelId, 1062 | getFallbackMaxTokens, 1063 | getFallbackTemperature, 1064 | getBaseUrlForRole, 1065 | // Global setting getters (No env var overrides) 1066 | getLogLevel, 1067 | getDebugFlag, 1068 | getDefaultNumTasks, 1069 | getDefaultSubtasks, 1070 | getDefaultPriority, 1071 | getProjectName, 1072 | getOllamaBaseURL, 1073 | getAzureBaseURL, 1074 | getBedrockBaseURL, 1075 | getResponseLanguage, 1076 | getCodebaseAnalysisEnabled, 1077 | isCodebaseAnalysisEnabled, 1078 | getParametersForRole, 1079 | getUserId, 1080 | // API Key Checkers (still relevant) 1081 | isApiKeySet, 1082 | getMcpApiKeyStatus, 1083 | // ADD: Function to get all provider names 1084 | getAllProviders, 1085 | getVertexProjectId, 1086 | getVertexLocation 1087 | }; 1088 | ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/list-tasks.js: -------------------------------------------------------------------------------- ```javascript 1 | import chalk from 'chalk'; 2 | import boxen from 'boxen'; 3 | import Table from 'cli-table3'; 4 | 5 | import { 6 | log, 7 | readJSON, 8 | truncate, 9 | readComplexityReport, 10 | addComplexityToTask 11 | } from '../utils.js'; 12 | import findNextTask from './find-next-task.js'; 13 | 14 | import { 15 | displayBanner, 16 | getStatusWithColor, 17 | formatDependenciesWithStatus, 18 | getComplexityWithColor, 19 | createProgressBar 20 | } from '../ui.js'; 21 | 22 | /** 23 | * List all tasks 24 | * @param {string} tasksPath - Path to the tasks.json file 25 | * @param {string} statusFilter - Filter by status (single status or comma-separated list, e.g., 'pending' or 'blocked,deferred') 26 | * @param {string} reportPath - Path to the complexity report 27 | * @param {boolean} withSubtasks - Whether to show subtasks 28 | * @param {string} outputFormat - Output format (text or json) 29 | * @param {Object} context - Context object (required) 30 | * @param {string} context.projectRoot - Project root path 31 | * @param {string} context.tag - Tag for the task 32 | * @returns {Object} - Task list result for json format 33 | */ 34 | function listTasks( 35 | tasksPath, 36 | statusFilter, 37 | reportPath = null, 38 | withSubtasks = false, 39 | outputFormat = 'text', 40 | context = {} 41 | ) { 42 | const { projectRoot, tag } = context; 43 | try { 44 | // Extract projectRoot from context if provided 45 | const data = readJSON(tasksPath, projectRoot, tag); // Pass projectRoot to readJSON 46 | if (!data || !data.tasks) { 47 | throw new Error(`No valid tasks found in ${tasksPath}`); 48 | } 49 | 50 | // Add complexity scores to tasks if report exists 51 | // `reportPath` is already tag-aware (resolved at the CLI boundary). 52 | const complexityReport = readComplexityReport(reportPath); 53 | // Apply complexity scores to tasks 54 | if (complexityReport && complexityReport.complexityAnalysis) { 55 | data.tasks.forEach((task) => addComplexityToTask(task, complexityReport)); 56 | } 57 | 58 | // Filter tasks by status if specified - now supports comma-separated statuses 59 | let filteredTasks; 60 | if (statusFilter && statusFilter.toLowerCase() !== 'all') { 61 | // Handle comma-separated statuses 62 | const allowedStatuses = statusFilter 63 | .split(',') 64 | .map((s) => s.trim().toLowerCase()) 65 | .filter((s) => s.length > 0); // Remove empty strings 66 | 67 | filteredTasks = data.tasks.filter( 68 | (task) => 69 | task.status && allowedStatuses.includes(task.status.toLowerCase()) 70 | ); 71 | } else { 72 | // Default to all tasks if no filter or filter is 'all' 73 | filteredTasks = data.tasks; 74 | } 75 | 76 | // Calculate completion statistics 77 | const totalTasks = data.tasks.length; 78 | const completedTasks = data.tasks.filter( 79 | (task) => task.status === 'done' || task.status === 'completed' 80 | ).length; 81 | const completionPercentage = 82 | totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; 83 | 84 | // Count statuses for tasks 85 | const doneCount = completedTasks; 86 | const inProgressCount = data.tasks.filter( 87 | (task) => task.status === 'in-progress' 88 | ).length; 89 | const pendingCount = data.tasks.filter( 90 | (task) => task.status === 'pending' 91 | ).length; 92 | const blockedCount = data.tasks.filter( 93 | (task) => task.status === 'blocked' 94 | ).length; 95 | const deferredCount = data.tasks.filter( 96 | (task) => task.status === 'deferred' 97 | ).length; 98 | const cancelledCount = data.tasks.filter( 99 | (task) => task.status === 'cancelled' 100 | ).length; 101 | const reviewCount = data.tasks.filter( 102 | (task) => task.status === 'review' 103 | ).length; 104 | 105 | // Count subtasks and their statuses 106 | let totalSubtasks = 0; 107 | let completedSubtasks = 0; 108 | let inProgressSubtasks = 0; 109 | let pendingSubtasks = 0; 110 | let blockedSubtasks = 0; 111 | let deferredSubtasks = 0; 112 | let cancelledSubtasks = 0; 113 | let reviewSubtasks = 0; 114 | 115 | data.tasks.forEach((task) => { 116 | if (task.subtasks && task.subtasks.length > 0) { 117 | totalSubtasks += task.subtasks.length; 118 | completedSubtasks += task.subtasks.filter( 119 | (st) => st.status === 'done' || st.status === 'completed' 120 | ).length; 121 | inProgressSubtasks += task.subtasks.filter( 122 | (st) => st.status === 'in-progress' 123 | ).length; 124 | pendingSubtasks += task.subtasks.filter( 125 | (st) => st.status === 'pending' 126 | ).length; 127 | blockedSubtasks += task.subtasks.filter( 128 | (st) => st.status === 'blocked' 129 | ).length; 130 | deferredSubtasks += task.subtasks.filter( 131 | (st) => st.status === 'deferred' 132 | ).length; 133 | cancelledSubtasks += task.subtasks.filter( 134 | (st) => st.status === 'cancelled' 135 | ).length; 136 | reviewSubtasks += task.subtasks.filter( 137 | (st) => st.status === 'review' 138 | ).length; 139 | } 140 | }); 141 | 142 | const subtaskCompletionPercentage = 143 | totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0; 144 | 145 | // Calculate dependency statistics (moved up to be available for all output formats) 146 | const completedTaskIds = new Set( 147 | data.tasks 148 | .filter((t) => t.status === 'done' || t.status === 'completed') 149 | .map((t) => t.id) 150 | ); 151 | 152 | const tasksWithNoDeps = data.tasks.filter( 153 | (t) => 154 | t.status !== 'done' && 155 | t.status !== 'completed' && 156 | (!t.dependencies || t.dependencies.length === 0) 157 | ).length; 158 | 159 | const tasksWithAllDepsSatisfied = data.tasks.filter( 160 | (t) => 161 | t.status !== 'done' && 162 | t.status !== 'completed' && 163 | t.dependencies && 164 | t.dependencies.length > 0 && 165 | t.dependencies.every((depId) => completedTaskIds.has(depId)) 166 | ).length; 167 | 168 | const tasksWithUnsatisfiedDeps = data.tasks.filter( 169 | (t) => 170 | t.status !== 'done' && 171 | t.status !== 'completed' && 172 | t.dependencies && 173 | t.dependencies.length > 0 && 174 | !t.dependencies.every((depId) => completedTaskIds.has(depId)) 175 | ).length; 176 | 177 | // Calculate total tasks ready to work on (no deps + satisfied deps) 178 | const tasksReadyToWork = tasksWithNoDeps + tasksWithAllDepsSatisfied; 179 | 180 | // Calculate most depended-on tasks 181 | const dependencyCount = {}; 182 | data.tasks.forEach((task) => { 183 | if (task.dependencies && task.dependencies.length > 0) { 184 | task.dependencies.forEach((depId) => { 185 | dependencyCount[depId] = (dependencyCount[depId] || 0) + 1; 186 | }); 187 | } 188 | }); 189 | 190 | // Find the most depended-on task 191 | let mostDependedOnTaskId = null; 192 | let maxDependents = 0; 193 | 194 | for (const [taskId, count] of Object.entries(dependencyCount)) { 195 | if (count > maxDependents) { 196 | maxDependents = count; 197 | mostDependedOnTaskId = parseInt(taskId); 198 | } 199 | } 200 | 201 | // Get the most depended-on task 202 | const mostDependedOnTask = 203 | mostDependedOnTaskId !== null 204 | ? data.tasks.find((t) => t.id === mostDependedOnTaskId) 205 | : null; 206 | 207 | // Calculate average dependencies per task 208 | const totalDependencies = data.tasks.reduce( 209 | (sum, task) => sum + (task.dependencies ? task.dependencies.length : 0), 210 | 0 211 | ); 212 | const avgDependenciesPerTask = totalDependencies / data.tasks.length; 213 | 214 | // Find next task to work on, passing the complexity report 215 | const nextItem = findNextTask(data.tasks, complexityReport); 216 | 217 | // For JSON output, return structured data 218 | if (outputFormat === 'json') { 219 | // *** Modification: Remove 'details' field for JSON output *** 220 | const tasksWithoutDetails = filteredTasks.map((task) => { 221 | // <-- USES filteredTasks! 222 | // Omit 'details' from the parent task 223 | const { details, ...taskRest } = task; 224 | 225 | // If subtasks exist, omit 'details' from them too 226 | if (taskRest.subtasks && Array.isArray(taskRest.subtasks)) { 227 | taskRest.subtasks = taskRest.subtasks.map((subtask) => { 228 | const { details: subtaskDetails, ...subtaskRest } = subtask; 229 | return subtaskRest; 230 | }); 231 | } 232 | return taskRest; 233 | }); 234 | // *** End of Modification *** 235 | 236 | return { 237 | tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED 238 | filter: statusFilter || 'all', // Return the actual filter used 239 | stats: { 240 | total: totalTasks, 241 | completed: doneCount, 242 | inProgress: inProgressCount, 243 | pending: pendingCount, 244 | blocked: blockedCount, 245 | deferred: deferredCount, 246 | cancelled: cancelledCount, 247 | review: reviewCount, 248 | completionPercentage, 249 | subtasks: { 250 | total: totalSubtasks, 251 | completed: completedSubtasks, 252 | inProgress: inProgressSubtasks, 253 | pending: pendingSubtasks, 254 | blocked: blockedSubtasks, 255 | deferred: deferredSubtasks, 256 | cancelled: cancelledSubtasks, 257 | completionPercentage: subtaskCompletionPercentage 258 | } 259 | } 260 | }; 261 | } 262 | 263 | // For markdown-readme output, return formatted markdown 264 | if (outputFormat === 'markdown-readme') { 265 | return generateMarkdownOutput(data, filteredTasks, { 266 | totalTasks, 267 | completedTasks, 268 | completionPercentage, 269 | doneCount, 270 | inProgressCount, 271 | pendingCount, 272 | blockedCount, 273 | deferredCount, 274 | cancelledCount, 275 | totalSubtasks, 276 | completedSubtasks, 277 | subtaskCompletionPercentage, 278 | inProgressSubtasks, 279 | pendingSubtasks, 280 | blockedSubtasks, 281 | deferredSubtasks, 282 | cancelledSubtasks, 283 | reviewSubtasks, 284 | tasksWithNoDeps, 285 | tasksReadyToWork, 286 | tasksWithUnsatisfiedDeps, 287 | mostDependedOnTask, 288 | mostDependedOnTaskId, 289 | maxDependents, 290 | avgDependenciesPerTask, 291 | complexityReport, 292 | withSubtasks, 293 | nextItem 294 | }); 295 | } 296 | 297 | // For compact output, return minimal one-line format 298 | if (outputFormat === 'compact') { 299 | return renderCompactOutput(filteredTasks, withSubtasks); 300 | } 301 | 302 | // ... existing code for text output ... 303 | 304 | // Calculate status breakdowns as percentages of total 305 | const taskStatusBreakdown = { 306 | 'in-progress': totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0, 307 | pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0, 308 | blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0, 309 | deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0, 310 | cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0, 311 | review: totalTasks > 0 ? (reviewCount / totalTasks) * 100 : 0 312 | }; 313 | 314 | const subtaskStatusBreakdown = { 315 | 'in-progress': 316 | totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0, 317 | pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0, 318 | blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0, 319 | deferred: 320 | totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0, 321 | cancelled: 322 | totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0, 323 | review: totalSubtasks > 0 ? (reviewSubtasks / totalSubtasks) * 100 : 0 324 | }; 325 | 326 | // Create progress bars with status breakdowns 327 | const taskProgressBar = createProgressBar( 328 | completionPercentage, 329 | 30, 330 | taskStatusBreakdown 331 | ); 332 | const subtaskProgressBar = createProgressBar( 333 | subtaskCompletionPercentage, 334 | 30, 335 | subtaskStatusBreakdown 336 | ); 337 | 338 | // Get terminal width - more reliable method 339 | let terminalWidth; 340 | try { 341 | // Try to get the actual terminal columns 342 | terminalWidth = process.stdout.columns; 343 | } catch (e) { 344 | // Fallback if columns cannot be determined 345 | log('debug', 'Could not determine terminal width, using default'); 346 | } 347 | // Ensure we have a reasonable default if detection fails 348 | terminalWidth = terminalWidth || 80; 349 | 350 | // Ensure terminal width is at least a minimum value to prevent layout issues 351 | terminalWidth = Math.max(terminalWidth, 80); 352 | 353 | // Create dashboard content 354 | const projectDashboardContent = 355 | chalk.white.bold('Project Dashboard') + 356 | '\n' + 357 | `Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` + 358 | `Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` + 359 | `Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` + 360 | `Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` + 361 | chalk.cyan.bold('Priority Breakdown:') + 362 | '\n' + 363 | `${chalk.red('•')} ${chalk.white('High priority:')} ${data.tasks.filter((t) => t.priority === 'high').length}\n` + 364 | `${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${data.tasks.filter((t) => t.priority === 'medium').length}\n` + 365 | `${chalk.green('•')} ${chalk.white('Low priority:')} ${data.tasks.filter((t) => t.priority === 'low').length}`; 366 | 367 | const dependencyDashboardContent = 368 | chalk.white.bold('Dependency Status & Next Task') + 369 | '\n' + 370 | chalk.cyan.bold('Dependency Metrics:') + 371 | '\n' + 372 | `${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${tasksWithNoDeps}\n` + 373 | `${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${tasksReadyToWork}\n` + 374 | `${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${tasksWithUnsatisfiedDeps}\n` + 375 | `${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray('None')}\n` + 376 | `${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${avgDependenciesPerTask.toFixed(1)}\n\n` + 377 | chalk.cyan.bold('Next Task to Work On:') + 378 | '\n' + 379 | `ID: ${chalk.cyan(nextItem ? nextItem.id : 'N/A')} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow('No task available')} 380 | ` + 381 | `Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ''} 382 | ` + 383 | `Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray('N/A')}`; 384 | 385 | // Calculate width for side-by-side display 386 | // Box borders, padding take approximately 4 chars on each side 387 | const minDashboardWidth = 50; // Minimum width for dashboard 388 | const minDependencyWidth = 50; // Minimum width for dependency dashboard 389 | const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; // Extra 4 chars for spacing 390 | 391 | // If terminal is wide enough, show boxes side by side with responsive widths 392 | if (terminalWidth >= totalMinWidth) { 393 | // Calculate widths proportionally for each box - use exact 50% width each 394 | const availableWidth = terminalWidth; 395 | const halfWidth = Math.floor(availableWidth / 2); 396 | 397 | // Account for border characters (2 chars on each side) 398 | const boxContentWidth = halfWidth - 4; 399 | 400 | // Create boxen options with precise widths 401 | const dashboardBox = boxen(projectDashboardContent, { 402 | padding: 1, 403 | borderColor: 'blue', 404 | borderStyle: 'round', 405 | width: boxContentWidth, 406 | dimBorder: false 407 | }); 408 | 409 | const dependencyBox = boxen(dependencyDashboardContent, { 410 | padding: 1, 411 | borderColor: 'magenta', 412 | borderStyle: 'round', 413 | width: boxContentWidth, 414 | dimBorder: false 415 | }); 416 | 417 | // Create a better side-by-side layout with exact spacing 418 | const dashboardLines = dashboardBox.split('\n'); 419 | const dependencyLines = dependencyBox.split('\n'); 420 | 421 | // Make sure both boxes have the same height 422 | const maxHeight = Math.max(dashboardLines.length, dependencyLines.length); 423 | 424 | // For each line of output, pad the dashboard line to exactly halfWidth chars 425 | // This ensures the dependency box starts at exactly the right position 426 | const combinedLines = []; 427 | for (let i = 0; i < maxHeight; i++) { 428 | // Get the dashboard line (or empty string if we've run out of lines) 429 | const dashLine = i < dashboardLines.length ? dashboardLines[i] : ''; 430 | // Get the dependency line (or empty string if we've run out of lines) 431 | const depLine = i < dependencyLines.length ? dependencyLines[i] : ''; 432 | 433 | // Remove any trailing spaces from dashLine before padding to exact width 434 | const trimmedDashLine = dashLine.trimEnd(); 435 | // Pad the dashboard line to exactly halfWidth chars with no extra spaces 436 | const paddedDashLine = trimmedDashLine.padEnd(halfWidth, ' '); 437 | 438 | // Join the lines with no space in between 439 | combinedLines.push(paddedDashLine + depLine); 440 | } 441 | 442 | // Join all lines and output 443 | console.log(combinedLines.join('\n')); 444 | } else { 445 | // Terminal too narrow, show boxes stacked vertically 446 | const dashboardBox = boxen(projectDashboardContent, { 447 | padding: 1, 448 | borderColor: 'blue', 449 | borderStyle: 'round', 450 | margin: { top: 0, bottom: 1 } 451 | }); 452 | 453 | const dependencyBox = boxen(dependencyDashboardContent, { 454 | padding: 1, 455 | borderColor: 'magenta', 456 | borderStyle: 'round', 457 | margin: { top: 0, bottom: 1 } 458 | }); 459 | 460 | // Display stacked vertically 461 | console.log(dashboardBox); 462 | console.log(dependencyBox); 463 | } 464 | 465 | if (filteredTasks.length === 0) { 466 | console.log( 467 | boxen( 468 | statusFilter 469 | ? chalk.yellow(`No tasks with status '${statusFilter}' found`) 470 | : chalk.yellow('No tasks found'), 471 | { padding: 1, borderColor: 'yellow', borderStyle: 'round' } 472 | ) 473 | ); 474 | return; 475 | } 476 | 477 | // COMPLETELY REVISED TABLE APPROACH 478 | // Define percentage-based column widths and calculate actual widths 479 | // Adjust percentages based on content type and user requirements 480 | 481 | // Adjust ID width if showing subtasks (subtask IDs are longer: e.g., "1.2") 482 | const idWidthPct = withSubtasks ? 10 : 7; 483 | 484 | // Calculate max status length to accommodate "in-progress" 485 | const statusWidthPct = 15; 486 | 487 | // Increase priority column width as requested 488 | const priorityWidthPct = 12; 489 | 490 | // Make dependencies column smaller as requested (-20%) 491 | const depsWidthPct = 20; 492 | 493 | const complexityWidthPct = 10; 494 | 495 | // Calculate title/description width as remaining space (+20% from dependencies reduction) 496 | const titleWidthPct = 497 | 100 - 498 | idWidthPct - 499 | statusWidthPct - 500 | priorityWidthPct - 501 | depsWidthPct - 502 | complexityWidthPct; 503 | 504 | // Allow 10 characters for borders and padding 505 | const availableWidth = terminalWidth - 10; 506 | 507 | // Calculate actual column widths based on percentages 508 | const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); 509 | const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); 510 | const priorityWidth = Math.floor(availableWidth * (priorityWidthPct / 100)); 511 | const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); 512 | const complexityWidth = Math.floor( 513 | availableWidth * (complexityWidthPct / 100) 514 | ); 515 | const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); 516 | 517 | // Create a table with correct borders and spacing 518 | const table = new Table({ 519 | head: [ 520 | chalk.cyan.bold('ID'), 521 | chalk.cyan.bold('Title'), 522 | chalk.cyan.bold('Status'), 523 | chalk.cyan.bold('Priority'), 524 | chalk.cyan.bold('Dependencies'), 525 | chalk.cyan.bold('Complexity') 526 | ], 527 | colWidths: [ 528 | idWidth, 529 | titleWidth, 530 | statusWidth, 531 | priorityWidth, 532 | depsWidth, 533 | complexityWidth // Added complexity column width 534 | ], 535 | style: { 536 | head: [], // No special styling for header 537 | border: [], // No special styling for border 538 | compact: false // Use default spacing 539 | }, 540 | wordWrap: true, 541 | wrapOnWordBoundary: true 542 | }); 543 | 544 | // Process tasks for the table 545 | filteredTasks.forEach((task) => { 546 | // Format dependencies with status indicators (colored) 547 | let depText = 'None'; 548 | if (task.dependencies && task.dependencies.length > 0) { 549 | // Use the proper formatDependenciesWithStatus function for colored status 550 | depText = formatDependenciesWithStatus( 551 | task.dependencies, 552 | data.tasks, 553 | true, 554 | complexityReport 555 | ); 556 | } else { 557 | depText = chalk.gray('None'); 558 | } 559 | 560 | // Clean up any ANSI codes or confusing characters 561 | const cleanTitle = task.title.replace(/\n/g, ' '); 562 | 563 | // Get priority color 564 | const priorityColor = 565 | { 566 | high: chalk.red, 567 | medium: chalk.yellow, 568 | low: chalk.gray 569 | }[task.priority || 'medium'] || chalk.white; 570 | 571 | // Format status 572 | const status = getStatusWithColor(task.status, true); 573 | 574 | // Add the row without truncating dependencies 575 | table.push([ 576 | task.id.toString(), 577 | truncate(cleanTitle, titleWidth - 3), 578 | status, 579 | priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)), 580 | depText, 581 | task.complexityScore 582 | ? getComplexityWithColor(task.complexityScore) 583 | : chalk.gray('N/A') 584 | ]); 585 | 586 | // Add subtasks if requested 587 | if (withSubtasks && task.subtasks && task.subtasks.length > 0) { 588 | task.subtasks.forEach((subtask) => { 589 | // Format subtask dependencies with status indicators 590 | let subtaskDepText = 'None'; 591 | if (subtask.dependencies && subtask.dependencies.length > 0) { 592 | // Handle both subtask-to-subtask and subtask-to-task dependencies 593 | const formattedDeps = subtask.dependencies 594 | .map((depId) => { 595 | // Check if it's a dependency on another subtask 596 | if (typeof depId === 'number' && depId < 100) { 597 | const foundSubtask = task.subtasks.find( 598 | (st) => st.id === depId 599 | ); 600 | if (foundSubtask) { 601 | const isDone = 602 | foundSubtask.status === 'done' || 603 | foundSubtask.status === 'completed'; 604 | const isInProgress = foundSubtask.status === 'in-progress'; 605 | 606 | // Use consistent color formatting instead of emojis 607 | if (isDone) { 608 | return chalk.green.bold(`${task.id}.${depId}`); 609 | } else if (isInProgress) { 610 | return chalk.hex('#FFA500').bold(`${task.id}.${depId}`); 611 | } else { 612 | return chalk.red.bold(`${task.id}.${depId}`); 613 | } 614 | } 615 | } 616 | // Default to regular task dependency 617 | const depTask = data.tasks.find((t) => t.id === depId); 618 | if (depTask) { 619 | // Add complexity to depTask before checking status 620 | addComplexityToTask(depTask, complexityReport); 621 | const isDone = 622 | depTask.status === 'done' || depTask.status === 'completed'; 623 | const isInProgress = depTask.status === 'in-progress'; 624 | // Use the same color scheme as in formatDependenciesWithStatus 625 | if (isDone) { 626 | return chalk.green.bold(`${depId}`); 627 | } else if (isInProgress) { 628 | return chalk.hex('#FFA500').bold(`${depId}`); 629 | } else { 630 | return chalk.red.bold(`${depId}`); 631 | } 632 | } 633 | return chalk.cyan(depId.toString()); 634 | }) 635 | .join(', '); 636 | 637 | subtaskDepText = formattedDeps || chalk.gray('None'); 638 | } 639 | 640 | // Add the subtask row without truncating dependencies 641 | table.push([ 642 | `${task.id}.${subtask.id}`, 643 | chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`), 644 | getStatusWithColor(subtask.status, true), 645 | chalk.dim('-'), 646 | subtaskDepText, 647 | subtask.complexityScore 648 | ? chalk.gray(`${subtask.complexityScore}`) 649 | : chalk.gray('N/A') 650 | ]); 651 | }); 652 | } 653 | }); 654 | 655 | // Ensure we output the table even if it had to wrap 656 | try { 657 | console.log(table.toString()); 658 | } catch (err) { 659 | log('error', `Error rendering table: ${err.message}`); 660 | 661 | // Fall back to simpler output 662 | console.log( 663 | chalk.yellow( 664 | '\nFalling back to simple task list due to terminal width constraints:' 665 | ) 666 | ); 667 | filteredTasks.forEach((task) => { 668 | console.log( 669 | `${chalk.cyan(task.id)}: ${chalk.white(task.title)} - ${getStatusWithColor(task.status)}` 670 | ); 671 | }); 672 | } 673 | 674 | // Show filter info if applied 675 | if (statusFilter) { 676 | console.log(chalk.yellow(`\nFiltered by status: ${statusFilter}`)); 677 | console.log( 678 | chalk.yellow(`Showing ${filteredTasks.length} of ${totalTasks} tasks`) 679 | ); 680 | } 681 | 682 | // Define priority colors 683 | const priorityColors = { 684 | high: chalk.red.bold, 685 | medium: chalk.yellow, 686 | low: chalk.gray 687 | }; 688 | 689 | // Show next task box in a prominent color 690 | if (nextItem) { 691 | // Prepare subtasks section if they exist (Only tasks have .subtasks property) 692 | let subtasksSection = ''; 693 | // Check if the nextItem is a top-level task before looking for subtasks 694 | const parentTaskForSubtasks = data.tasks.find( 695 | (t) => String(t.id) === String(nextItem.id) 696 | ); // Find the original task object 697 | if ( 698 | parentTaskForSubtasks && 699 | parentTaskForSubtasks.subtasks && 700 | parentTaskForSubtasks.subtasks.length > 0 701 | ) { 702 | subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`; 703 | subtasksSection += parentTaskForSubtasks.subtasks 704 | .map((subtask) => { 705 | // Add complexity to subtask before display 706 | addComplexityToTask(subtask, complexityReport); 707 | // Using a more simplified format for subtask status display 708 | const status = subtask.status || 'pending'; 709 | const statusColors = { 710 | done: chalk.green, 711 | completed: chalk.green, 712 | pending: chalk.yellow, 713 | 'in-progress': chalk.blue, 714 | deferred: chalk.gray, 715 | blocked: chalk.red, 716 | cancelled: chalk.gray 717 | }; 718 | const statusColor = 719 | statusColors[status.toLowerCase()] || chalk.white; 720 | // Ensure subtask ID is displayed correctly using parent ID from the original task object 721 | return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`; 722 | }) 723 | .join('\n'); 724 | } 725 | 726 | console.log( 727 | boxen( 728 | chalk.hex('#FF8800').bold( 729 | // Use nextItem.id and nextItem.title 730 | `🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}` 731 | ) + 732 | '\n\n' + 733 | // Use nextItem.priority, nextItem.status, nextItem.dependencies 734 | `${chalk.white('Priority:')} ${priorityColors[nextItem.priority || 'medium'](nextItem.priority || 'medium')} ${chalk.white('Status:')} ${getStatusWithColor(nextItem.status, true)}\n` + 735 | `${chalk.white('Dependencies:')} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray('None')}\n\n` + 736 | // Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this) 737 | // *** Fetching original item for description and details *** 738 | `${chalk.white('Description:')} ${getWorkItemDescription(nextItem, data.tasks)}` + 739 | subtasksSection + // <-- Subtasks are handled above now 740 | '\n\n' + 741 | // Use nextItem.id 742 | `${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` + 743 | // Use nextItem.id 744 | `${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${nextItem.id}`)}`, 745 | { 746 | padding: { left: 2, right: 2, top: 1, bottom: 1 }, 747 | borderColor: '#FF8800', 748 | borderStyle: 'round', 749 | margin: { top: 1, bottom: 1 }, 750 | title: '⚡ RECOMMENDED NEXT TASK ⚡', 751 | titleAlignment: 'center', 752 | width: terminalWidth - 4, 753 | fullscreen: false 754 | } 755 | ) 756 | ); 757 | } else { 758 | console.log( 759 | boxen( 760 | chalk.hex('#FF8800').bold('No eligible next task found') + 761 | '\n\n' + 762 | 'All pending tasks have dependencies that are not yet completed, or all tasks are done.', 763 | { 764 | padding: 1, 765 | borderColor: '#FF8800', 766 | borderStyle: 'round', 767 | margin: { top: 1, bottom: 1 }, 768 | title: '⚡ NEXT TASK ⚡', 769 | titleAlignment: 'center', 770 | width: terminalWidth - 4 // Use full terminal width minus a small margin 771 | } 772 | ) 773 | ); 774 | } 775 | 776 | // Show next steps 777 | console.log( 778 | boxen( 779 | chalk.white.bold('Suggested Next Steps:') + 780 | '\n\n' + 781 | `${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next\n` + 782 | `${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` + 783 | `${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`, 784 | { 785 | padding: 1, 786 | borderColor: 'gray', 787 | borderStyle: 'round', 788 | margin: { top: 1 } 789 | } 790 | ) 791 | ); 792 | } catch (error) { 793 | log('error', `Error listing tasks: ${error.message}`); 794 | 795 | if (outputFormat === 'json') { 796 | // Return structured error for JSON output 797 | throw { 798 | code: 'TASK_LIST_ERROR', 799 | message: error.message, 800 | details: error.stack 801 | }; 802 | } 803 | 804 | console.error(chalk.red(`Error: ${error.message}`)); 805 | process.exit(1); 806 | } 807 | } 808 | 809 | // *** Helper function to get description for task or subtask *** 810 | function getWorkItemDescription(item, allTasks) { 811 | if (!item) return 'N/A'; 812 | if (item.parentId) { 813 | // It's a subtask 814 | const parent = allTasks.find((t) => t.id === item.parentId); 815 | const subtask = parent?.subtasks?.find( 816 | (st) => `${parent.id}.${st.id}` === item.id 817 | ); 818 | return subtask?.description || 'No description available.'; 819 | } else { 820 | // It's a top-level task 821 | const task = allTasks.find((t) => String(t.id) === String(item.id)); 822 | return task?.description || 'No description available.'; 823 | } 824 | } 825 | 826 | /** 827 | * Generate markdown-formatted output for README files 828 | * @param {Object} data - Full tasks data 829 | * @param {Array} filteredTasks - Filtered tasks array 830 | * @param {Object} stats - Statistics object 831 | * @returns {string} - Formatted markdown string 832 | */ 833 | function generateMarkdownOutput(data, filteredTasks, stats) { 834 | const { 835 | totalTasks, 836 | completedTasks, 837 | completionPercentage, 838 | doneCount, 839 | inProgressCount, 840 | pendingCount, 841 | blockedCount, 842 | deferredCount, 843 | cancelledCount, 844 | totalSubtasks, 845 | completedSubtasks, 846 | subtaskCompletionPercentage, 847 | inProgressSubtasks, 848 | pendingSubtasks, 849 | blockedSubtasks, 850 | deferredSubtasks, 851 | cancelledSubtasks, 852 | tasksWithNoDeps, 853 | tasksReadyToWork, 854 | tasksWithUnsatisfiedDeps, 855 | mostDependedOnTask, 856 | mostDependedOnTaskId, 857 | maxDependents, 858 | avgDependenciesPerTask, 859 | complexityReport, 860 | withSubtasks, 861 | nextItem 862 | } = stats; 863 | 864 | let markdown = ''; 865 | 866 | // Create progress bars for markdown (using Unicode block characters) 867 | const createMarkdownProgressBar = (percentage, width = 20) => { 868 | const filled = Math.round((percentage / 100) * width); 869 | const empty = width - filled; 870 | return '█'.repeat(filled) + '░'.repeat(empty); 871 | }; 872 | 873 | const taskProgressBar = createMarkdownProgressBar(completionPercentage, 20); 874 | const subtaskProgressBar = createMarkdownProgressBar( 875 | subtaskCompletionPercentage, 876 | 20 877 | ); 878 | 879 | // Dashboard section 880 | // markdown += '```\n'; 881 | markdown += '| Project Dashboard | |\n'; 882 | markdown += '| :- |:-|\n'; 883 | markdown += `| Task Progress | ${taskProgressBar} ${Math.round(completionPercentage)}% |\n`; 884 | markdown += `| Done | ${doneCount} |\n`; 885 | markdown += `| In Progress | ${inProgressCount} |\n`; 886 | markdown += `| Pending | ${pendingCount} |\n`; 887 | markdown += `| Deferred | ${deferredCount} |\n`; 888 | markdown += `| Cancelled | ${cancelledCount} |\n`; 889 | markdown += `|-|-|\n`; 890 | markdown += `| Subtask Progress | ${subtaskProgressBar} ${Math.round(subtaskCompletionPercentage)}% |\n`; 891 | markdown += `| Completed | ${completedSubtasks} |\n`; 892 | markdown += `| In Progress | ${inProgressSubtasks} |\n`; 893 | markdown += `| Pending | ${pendingSubtasks} |\n`; 894 | 895 | markdown += '\n\n'; 896 | 897 | // Tasks table 898 | markdown += 899 | '| ID | Title | Status | Priority | Dependencies | Complexity |\n'; 900 | markdown += 901 | '| :- | :- | :- | :- | :- | :- |\n'; 902 | 903 | // Helper function to format status with symbols 904 | const getStatusSymbol = (status) => { 905 | switch (status) { 906 | case 'done': 907 | case 'completed': 908 | return '✓ done'; 909 | case 'in-progress': 910 | return '► in-progress'; 911 | case 'pending': 912 | return '○ pending'; 913 | case 'blocked': 914 | return '⭕ blocked'; 915 | case 'deferred': 916 | return 'x deferred'; 917 | case 'cancelled': 918 | return 'x cancelled'; 919 | case 'review': 920 | return '? review'; 921 | default: 922 | return status || 'pending'; 923 | } 924 | }; 925 | 926 | // Helper function to format dependencies without color codes 927 | const formatDependenciesForMarkdown = (deps, allTasks) => { 928 | if (!deps || deps.length === 0) return 'None'; 929 | return deps 930 | .map((depId) => { 931 | const depTask = allTasks.find((t) => t.id === depId); 932 | return depTask ? depId.toString() : depId.toString(); 933 | }) 934 | .join(', '); 935 | }; 936 | 937 | // Process all tasks 938 | filteredTasks.forEach((task) => { 939 | const taskTitle = task.title; // No truncation for README 940 | const statusSymbol = getStatusSymbol(task.status); 941 | const priority = task.priority || 'medium'; 942 | const deps = formatDependenciesForMarkdown(task.dependencies, data.tasks); 943 | const complexity = task.complexityScore 944 | ? `● ${task.complexityScore}` 945 | : 'N/A'; 946 | 947 | markdown += `| ${task.id} | ${taskTitle} | ${statusSymbol} | ${priority} | ${deps} | ${complexity} |\n`; 948 | 949 | // Add subtasks if requested 950 | if (withSubtasks && task.subtasks && task.subtasks.length > 0) { 951 | task.subtasks.forEach((subtask) => { 952 | const subtaskTitle = `${subtask.title}`; // No truncation 953 | const subtaskStatus = getStatusSymbol(subtask.status); 954 | const subtaskDeps = formatDependenciesForMarkdown( 955 | subtask.dependencies, 956 | data.tasks 957 | ); 958 | const subtaskComplexity = subtask.complexityScore 959 | ? subtask.complexityScore.toString() 960 | : 'N/A'; 961 | 962 | markdown += `| ${task.id}.${subtask.id} | ${subtaskTitle} | ${subtaskStatus} | - | ${subtaskDeps} | ${subtaskComplexity} |\n`; 963 | }); 964 | } 965 | }); 966 | 967 | return markdown; 968 | } 969 | 970 | /** 971 | * Format dependencies for compact output with truncation and coloring 972 | * @param {Array} dependencies - Array of dependency IDs 973 | * @returns {string} - Formatted dependency string with arrow prefix 974 | */ 975 | function formatCompactDependencies(dependencies) { 976 | if (!dependencies || dependencies.length === 0) { 977 | return ''; 978 | } 979 | 980 | if (dependencies.length > 5) { 981 | const visible = dependencies.slice(0, 5).join(','); 982 | const remaining = dependencies.length - 5; 983 | return ` → ${chalk.cyan(visible)}${chalk.gray('... (+' + remaining + ' more)')}`; 984 | } else { 985 | return ` → ${chalk.cyan(dependencies.join(','))}`; 986 | } 987 | } 988 | 989 | /** 990 | * Format a single task in compact one-line format 991 | * @param {Object} task - Task object 992 | * @param {number} maxTitleLength - Maximum title length before truncation 993 | * @returns {string} - Formatted task line 994 | */ 995 | function formatCompactTask(task, maxTitleLength = 50) { 996 | const status = task.status || 'pending'; 997 | const priority = task.priority || 'medium'; 998 | const title = truncate(task.title || 'Untitled', maxTitleLength); 999 | 1000 | // Use colored status from existing function 1001 | const coloredStatus = getStatusWithColor(status, true); 1002 | 1003 | // Color priority based on level 1004 | const priorityColors = { 1005 | high: chalk.red, 1006 | medium: chalk.yellow, 1007 | low: chalk.gray 1008 | }; 1009 | const priorityColor = priorityColors[priority] || chalk.white; 1010 | 1011 | // Format dependencies using shared helper 1012 | const depsText = formatCompactDependencies(task.dependencies); 1013 | 1014 | return `${chalk.cyan(task.id)} ${coloredStatus} ${chalk.white(title)} ${priorityColor('(' + priority + ')')}${depsText}`; 1015 | } 1016 | 1017 | /** 1018 | * Format a subtask in compact format with indentation 1019 | * @param {Object} subtask - Subtask object 1020 | * @param {string|number} parentId - Parent task ID 1021 | * @param {number} maxTitleLength - Maximum title length before truncation 1022 | * @returns {string} - Formatted subtask line 1023 | */ 1024 | function formatCompactSubtask(subtask, parentId, maxTitleLength = 47) { 1025 | const status = subtask.status || 'pending'; 1026 | const title = truncate(subtask.title || 'Untitled', maxTitleLength); 1027 | 1028 | // Use colored status from existing function 1029 | const coloredStatus = getStatusWithColor(status, true); 1030 | 1031 | // Format dependencies using shared helper 1032 | const depsText = formatCompactDependencies(subtask.dependencies); 1033 | 1034 | return ` ${chalk.cyan(parentId + '.' + subtask.id)} ${coloredStatus} ${chalk.dim(title)}${depsText}`; 1035 | } 1036 | 1037 | /** 1038 | * Render complete compact output 1039 | * @param {Array} filteredTasks - Tasks to display 1040 | * @param {boolean} withSubtasks - Whether to include subtasks 1041 | * @returns {void} - Outputs directly to console 1042 | */ 1043 | function renderCompactOutput(filteredTasks, withSubtasks) { 1044 | if (filteredTasks.length === 0) { 1045 | console.log('No tasks found'); 1046 | return; 1047 | } 1048 | 1049 | const output = []; 1050 | 1051 | filteredTasks.forEach((task) => { 1052 | output.push(formatCompactTask(task)); 1053 | 1054 | if (withSubtasks && task.subtasks && task.subtasks.length > 0) { 1055 | task.subtasks.forEach((subtask) => { 1056 | output.push(formatCompactSubtask(subtask, task.id)); 1057 | }); 1058 | } 1059 | }); 1060 | 1061 | console.log(output.join('\n')); 1062 | } 1063 | 1064 | export default listTasks; 1065 | ```