This is page 36 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 -------------------------------------------------------------------------------- /docs/models.md: -------------------------------------------------------------------------------- ```markdown 1 | # Available Models as of September 19, 2025 2 | 3 | ## Main Models 4 | 5 | | Provider | Model Name | SWE Score | Input Cost | Output Cost | 6 | | ----------- | ---------------------------------------------- | --------- | ---------- | ----------- | 7 | | anthropic | claude-sonnet-4-20250514 | 0.727 | 3 | 15 | 8 | | anthropic | claude-opus-4-20250514 | 0.725 | 15 | 75 | 9 | | anthropic | claude-3-7-sonnet-20250219 | 0.623 | 3 | 15 | 10 | | anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 | 11 | | claude-code | opus | 0.725 | 0 | 0 | 12 | | claude-code | sonnet | 0.727 | 0 | 0 | 13 | | mcp | mcp-sampling | — | 0 | 0 | 14 | | gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 | 15 | | gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 | 16 | | grok-cli | grok-4-latest | 0.7 | 0 | 0 | 17 | | grok-cli | grok-3-latest | 0.65 | 0 | 0 | 18 | | grok-cli | grok-3-fast | 0.6 | 0 | 0 | 19 | | grok-cli | grok-3-mini-fast | 0.55 | 0 | 0 | 20 | | openai | gpt-4o | 0.332 | 2.5 | 10 | 21 | | openai | o1 | 0.489 | 15 | 60 | 22 | | openai | o3 | 0.5 | 2 | 8 | 23 | | openai | o3-mini | 0.493 | 1.1 | 4.4 | 24 | | openai | o4-mini | 0.45 | 1.1 | 4.4 | 25 | | openai | o1-mini | 0.4 | 1.1 | 4.4 | 26 | | openai | o1-pro | — | 150 | 600 | 27 | | openai | gpt-4-5-preview | 0.38 | 75 | 150 | 28 | | openai | gpt-4-1-mini | — | 0.4 | 1.6 | 29 | | openai | gpt-4-1-nano | — | 0.1 | 0.4 | 30 | | openai | gpt-4o-mini | 0.3 | 0.15 | 0.6 | 31 | | openai | gpt-5 | 0.749 | 5 | 20 | 32 | | google | gemini-2.5-pro-preview-05-06 | 0.638 | — | — | 33 | | google | gemini-2.5-pro-preview-03-25 | 0.638 | — | — | 34 | | google | gemini-2.5-flash-preview-04-17 | 0.604 | — | — | 35 | | google | gemini-2.0-flash | 0.518 | 0.15 | 0.6 | 36 | | google | gemini-2.0-flash-lite | — | — | — | 37 | | xai | grok-3 | — | 3 | 15 | 38 | | xai | grok-3-fast | — | 5 | 25 | 39 | | xai | grok-4 | — | 3 | 15 | 40 | | groq | moonshotai/kimi-k2-instruct | 0.66 | 1 | 3 | 41 | | groq | llama-3.3-70b-versatile | 0.55 | 0.59 | 0.79 | 42 | | groq | llama-3.1-8b-instant | 0.32 | 0.05 | 0.08 | 43 | | groq | llama-4-scout | 0.45 | 0.11 | 0.34 | 44 | | groq | llama-4-maverick | 0.52 | 0.5 | 0.77 | 45 | | groq | mixtral-8x7b-32768 | 0.35 | 0.24 | 0.24 | 46 | | groq | qwen-qwq-32b-preview | 0.4 | 0.18 | 0.18 | 47 | | groq | deepseek-r1-distill-llama-70b | 0.52 | 0.75 | 0.99 | 48 | | groq | gemma2-9b-it | 0.3 | 0.2 | 0.2 | 49 | | groq | whisper-large-v3 | — | 0.11 | 0 | 50 | | perplexity | sonar-pro | — | 3 | 15 | 51 | | perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 | 52 | | perplexity | sonar-reasoning | 0.211 | 1 | 5 | 53 | | openrouter | google/gemini-2.5-flash-preview-05-20 | — | 0.15 | 0.6 | 54 | | openrouter | google/gemini-2.5-flash-preview-05-20:thinking | — | 0.15 | 3.5 | 55 | | openrouter | google/gemini-2.5-pro-exp-03-25 | — | 0 | 0 | 56 | | openrouter | deepseek/deepseek-chat-v3-0324 | — | 0.27 | 1.1 | 57 | | openrouter | openai/gpt-4.1 | — | 2 | 8 | 58 | | openrouter | openai/gpt-4.1-mini | — | 0.4 | 1.6 | 59 | | openrouter | openai/gpt-4.1-nano | — | 0.1 | 0.4 | 60 | | openrouter | openai/o3 | — | 10 | 40 | 61 | | openrouter | openai/codex-mini | — | 1.5 | 6 | 62 | | openrouter | openai/gpt-4o-mini | — | 0.15 | 0.6 | 63 | | openrouter | openai/o4-mini | 0.45 | 1.1 | 4.4 | 64 | | openrouter | openai/o4-mini-high | — | 1.1 | 4.4 | 65 | | openrouter | openai/o1-pro | — | 150 | 600 | 66 | | openrouter | meta-llama/llama-3.3-70b-instruct | — | 120 | 600 | 67 | | openrouter | meta-llama/llama-4-maverick | — | 0.18 | 0.6 | 68 | | openrouter | meta-llama/llama-4-scout | — | 0.08 | 0.3 | 69 | | openrouter | qwen/qwen-max | — | 1.6 | 6.4 | 70 | | openrouter | qwen/qwen-turbo | — | 0.05 | 0.2 | 71 | | openrouter | qwen/qwen3-235b-a22b | — | 0.14 | 2 | 72 | | openrouter | mistralai/mistral-small-3.1-24b-instruct | — | 0.1 | 0.3 | 73 | | openrouter | mistralai/devstral-small | — | 0.1 | 0.3 | 74 | | openrouter | mistralai/mistral-nemo | — | 0.03 | 0.07 | 75 | | ollama | gpt-oss:latest | 0.607 | 0 | 0 | 76 | | ollama | gpt-oss:20b | 0.607 | 0 | 0 | 77 | | ollama | gpt-oss:120b | 0.624 | 0 | 0 | 78 | | ollama | devstral:latest | — | 0 | 0 | 79 | | ollama | qwen3:latest | — | 0 | 0 | 80 | | ollama | qwen3:14b | — | 0 | 0 | 81 | | ollama | qwen3:32b | — | 0 | 0 | 82 | | ollama | mistral-small3.1:latest | — | 0 | 0 | 83 | | ollama | llama3.3:latest | — | 0 | 0 | 84 | | ollama | phi4:latest | — | 0 | 0 | 85 | | azure | gpt-4o | 0.332 | 2.5 | 10 | 86 | | azure | gpt-4o-mini | 0.3 | 0.15 | 0.6 | 87 | | azure | gpt-4-1 | — | 2 | 10 | 88 | | bedrock | us.anthropic.claude-3-haiku-20240307-v1:0 | 0.4 | 0.25 | 1.25 | 89 | | bedrock | us.anthropic.claude-3-opus-20240229-v1:0 | 0.725 | 15 | 75 | 90 | | bedrock | us.anthropic.claude-3-5-sonnet-20240620-v1:0 | 0.49 | 3 | 15 | 91 | | bedrock | us.anthropic.claude-3-5-sonnet-20241022-v2:0 | 0.49 | 3 | 15 | 92 | | bedrock | us.anthropic.claude-3-7-sonnet-20250219-v1:0 | 0.623 | 3 | 15 | 93 | | bedrock | us.anthropic.claude-3-5-haiku-20241022-v1:0 | 0.4 | 0.8 | 4 | 94 | | bedrock | us.anthropic.claude-opus-4-20250514-v1:0 | 0.725 | 15 | 75 | 95 | | bedrock | us.anthropic.claude-sonnet-4-20250514-v1:0 | 0.727 | 3 | 15 | 96 | 97 | ## Research Models 98 | 99 | | Provider | Model Name | SWE Score | Input Cost | Output Cost | 100 | | ----------- | -------------------------------------------- | --------- | ---------- | ----------- | 101 | | claude-code | opus | 0.725 | 0 | 0 | 102 | | claude-code | sonnet | 0.727 | 0 | 0 | 103 | | mcp | mcp-sampling | — | 0 | 0 | 104 | | gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 | 105 | | gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 | 106 | | grok-cli | grok-4-latest | 0.7 | 0 | 0 | 107 | | grok-cli | grok-3-latest | 0.65 | 0 | 0 | 108 | | grok-cli | grok-3-fast | 0.6 | 0 | 0 | 109 | | grok-cli | grok-3-mini-fast | 0.55 | 0 | 0 | 110 | | openai | gpt-4o-search-preview | 0.33 | 2.5 | 10 | 111 | | openai | gpt-4o-mini-search-preview | 0.3 | 0.15 | 0.6 | 112 | | xai | grok-3 | — | 3 | 15 | 113 | | xai | grok-3-fast | — | 5 | 25 | 114 | | xai | grok-4 | — | 3 | 15 | 115 | | groq | llama-3.3-70b-versatile | 0.55 | 0.59 | 0.79 | 116 | | groq | llama-4-scout | 0.45 | 0.11 | 0.34 | 117 | | groq | llama-4-maverick | 0.52 | 0.5 | 0.77 | 118 | | groq | qwen-qwq-32b-preview | 0.4 | 0.18 | 0.18 | 119 | | groq | deepseek-r1-distill-llama-70b | 0.52 | 0.75 | 0.99 | 120 | | perplexity | sonar-pro | — | 3 | 15 | 121 | | perplexity | sonar | — | 1 | 1 | 122 | | perplexity | deep-research | 0.211 | 2 | 8 | 123 | | perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 | 124 | | perplexity | sonar-reasoning | 0.211 | 1 | 5 | 125 | | bedrock | us.anthropic.claude-3-opus-20240229-v1:0 | 0.725 | 15 | 75 | 126 | | bedrock | us.anthropic.claude-3-5-sonnet-20240620-v1:0 | 0.49 | 3 | 15 | 127 | | bedrock | us.anthropic.claude-3-5-sonnet-20241022-v2:0 | 0.49 | 3 | 15 | 128 | | bedrock | us.anthropic.claude-3-7-sonnet-20250219-v1:0 | 0.623 | 3 | 15 | 129 | | bedrock | us.anthropic.claude-opus-4-20250514-v1:0 | 0.725 | 15 | 75 | 130 | | bedrock | us.anthropic.claude-sonnet-4-20250514-v1:0 | 0.727 | 3 | 15 | 131 | | bedrock | us.deepseek.r1-v1:0 | — | 1.35 | 5.4 | 132 | 133 | ## Fallback Models 134 | 135 | | Provider | Model Name | SWE Score | Input Cost | Output Cost | 136 | | ----------- | ---------------------------------------------- | --------- | ---------- | ----------- | 137 | | anthropic | claude-sonnet-4-20250514 | 0.727 | 3 | 15 | 138 | | anthropic | claude-opus-4-20250514 | 0.725 | 15 | 75 | 139 | | anthropic | claude-3-7-sonnet-20250219 | 0.623 | 3 | 15 | 140 | | anthropic | claude-3-5-sonnet-20241022 | 0.49 | 3 | 15 | 141 | | claude-code | opus | 0.725 | 0 | 0 | 142 | | claude-code | sonnet | 0.727 | 0 | 0 | 143 | | mcp | mcp-sampling | — | 0 | 0 | 144 | | gemini-cli | gemini-2.5-pro | 0.72 | 0 | 0 | 145 | | gemini-cli | gemini-2.5-flash | 0.71 | 0 | 0 | 146 | | grok-cli | grok-4-latest | 0.7 | 0 | 0 | 147 | | grok-cli | grok-3-latest | 0.65 | 0 | 0 | 148 | | grok-cli | grok-3-fast | 0.6 | 0 | 0 | 149 | | grok-cli | grok-3-mini-fast | 0.55 | 0 | 0 | 150 | | openai | gpt-4o | 0.332 | 2.5 | 10 | 151 | | openai | o3 | 0.5 | 2 | 8 | 152 | | openai | o4-mini | 0.45 | 1.1 | 4.4 | 153 | | openai | gpt-5 | 0.749 | 5 | 20 | 154 | | google | gemini-2.5-pro-preview-05-06 | 0.638 | — | — | 155 | | google | gemini-2.5-pro-preview-03-25 | 0.638 | — | — | 156 | | google | gemini-2.5-flash-preview-04-17 | 0.604 | — | — | 157 | | google | gemini-2.0-flash | 0.518 | 0.15 | 0.6 | 158 | | google | gemini-2.0-flash-lite | — | — | — | 159 | | xai | grok-3 | — | 3 | 15 | 160 | | xai | grok-3-fast | — | 5 | 25 | 161 | | xai | grok-4 | — | 3 | 15 | 162 | | groq | moonshotai/kimi-k2-instruct | 0.66 | 1 | 3 | 163 | | groq | llama-3.3-70b-versatile | 0.55 | 0.59 | 0.79 | 164 | | groq | llama-3.1-8b-instant | 0.32 | 0.05 | 0.08 | 165 | | groq | llama-4-scout | 0.45 | 0.11 | 0.34 | 166 | | groq | llama-4-maverick | 0.52 | 0.5 | 0.77 | 167 | | groq | mixtral-8x7b-32768 | 0.35 | 0.24 | 0.24 | 168 | | groq | qwen-qwq-32b-preview | 0.4 | 0.18 | 0.18 | 169 | | groq | gemma2-9b-it | 0.3 | 0.2 | 0.2 | 170 | | perplexity | sonar-reasoning-pro | 0.211 | 2 | 8 | 171 | | perplexity | sonar-reasoning | 0.211 | 1 | 5 | 172 | | openrouter | google/gemini-2.5-flash-preview-05-20 | — | 0.15 | 0.6 | 173 | | openrouter | google/gemini-2.5-flash-preview-05-20:thinking | — | 0.15 | 3.5 | 174 | | openrouter | google/gemini-2.5-pro-exp-03-25 | — | 0 | 0 | 175 | | openrouter | openai/gpt-4.1 | — | 2 | 8 | 176 | | openrouter | openai/gpt-4.1-mini | — | 0.4 | 1.6 | 177 | | openrouter | openai/gpt-4.1-nano | — | 0.1 | 0.4 | 178 | | openrouter | openai/o3 | — | 10 | 40 | 179 | | openrouter | openai/codex-mini | — | 1.5 | 6 | 180 | | openrouter | openai/gpt-4o-mini | — | 0.15 | 0.6 | 181 | | openrouter | openai/o4-mini | 0.45 | 1.1 | 4.4 | 182 | | openrouter | openai/o4-mini-high | — | 1.1 | 4.4 | 183 | | openrouter | openai/o1-pro | — | 150 | 600 | 184 | | openrouter | meta-llama/llama-3.3-70b-instruct | — | 120 | 600 | 185 | | openrouter | meta-llama/llama-4-maverick | — | 0.18 | 0.6 | 186 | | openrouter | meta-llama/llama-4-scout | — | 0.08 | 0.3 | 187 | | openrouter | qwen/qwen-max | — | 1.6 | 6.4 | 188 | | openrouter | qwen/qwen-turbo | — | 0.05 | 0.2 | 189 | | openrouter | qwen/qwen3-235b-a22b | — | 0.14 | 2 | 190 | | openrouter | mistralai/mistral-small-3.1-24b-instruct | — | 0.1 | 0.3 | 191 | | openrouter | mistralai/mistral-nemo | — | 0.03 | 0.07 | 192 | | ollama | gpt-oss:latest | 0.607 | 0 | 0 | 193 | | ollama | gpt-oss:20b | 0.607 | 0 | 0 | 194 | | ollama | gpt-oss:120b | 0.624 | 0 | 0 | 195 | | ollama | devstral:latest | — | 0 | 0 | 196 | | ollama | qwen3:latest | — | 0 | 0 | 197 | | ollama | qwen3:14b | — | 0 | 0 | 198 | | ollama | qwen3:32b | — | 0 | 0 | 199 | | ollama | mistral-small3.1:latest | — | 0 | 0 | 200 | | ollama | llama3.3:latest | — | 0 | 0 | 201 | | ollama | phi4:latest | — | 0 | 0 | 202 | | azure | gpt-4o | 0.332 | 2.5 | 10 | 203 | | azure | gpt-4o-mini | 0.3 | 0.15 | 0.6 | 204 | | azure | gpt-4-1 | — | 2 | 10 | 205 | | bedrock | us.anthropic.claude-3-haiku-20240307-v1:0 | 0.4 | 0.25 | 1.25 | 206 | | bedrock | us.anthropic.claude-3-opus-20240229-v1:0 | 0.725 | 15 | 75 | 207 | | bedrock | us.anthropic.claude-3-5-sonnet-20240620-v1:0 | 0.49 | 3 | 15 | 208 | | bedrock | us.anthropic.claude-3-5-sonnet-20241022-v2:0 | 0.49 | 3 | 15 | 209 | | bedrock | us.anthropic.claude-3-7-sonnet-20250219-v1:0 | 0.623 | 3 | 15 | 210 | | bedrock | us.anthropic.claude-3-5-haiku-20241022-v1:0 | 0.4 | 0.8 | 4 | 211 | | bedrock | us.anthropic.claude-opus-4-20250514-v1:0 | 0.725 | 15 | 75 | 212 | | bedrock | us.anthropic.claude-sonnet-4-20250514-v1:0 | 0.727 | 3 | 15 | 213 | 214 | ## Unsupported Models 215 | 216 | | Provider | Model Name | Reason | 217 | | ---------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 218 | | openrouter | deepseek/deepseek-chat-v3-0324:free | Free OpenRouter models are not supported due to severe rate limits, lack of tool use support, and other reliability issues that make them impractical for production use. | 219 | | openrouter | mistralai/mistral-small-3.1-24b-instruct:free | Free OpenRouter models are not supported due to severe rate limits, lack of tool use support, and other reliability issues that make them impractical for production use. | 220 | | openrouter | thudm/glm-4-32b:free | Free OpenRouter models are not supported due to severe rate limits, lack of tool use support, and other reliability issues that make them impractical for production use. | 221 | ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/models.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * models.js 3 | * Core functionality for managing AI model configurations 4 | */ 5 | 6 | import https from 'https'; 7 | import http from 'http'; 8 | import { 9 | getMainModelId, 10 | getResearchModelId, 11 | getFallbackModelId, 12 | getAvailableModels, 13 | getMainProvider, 14 | getResearchProvider, 15 | getFallbackProvider, 16 | isApiKeySet, 17 | getMcpApiKeyStatus, 18 | getConfig, 19 | writeConfig, 20 | isConfigFilePresent, 21 | getAllProviders, 22 | getBaseUrlForRole 23 | } from '../config-manager.js'; 24 | import { findConfigPath } from '../../../src/utils/path-utils.js'; 25 | import { log } from '../utils.js'; 26 | import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js'; 27 | 28 | // Constants 29 | const CONFIG_MISSING_ERROR = 30 | 'The configuration file is missing. Run "task-master init" to create it.'; 31 | 32 | /** 33 | * Fetches the list of models from OpenRouter API. 34 | * @returns {Promise<Array|null>} A promise that resolves with the list of model IDs or null if fetch fails. 35 | */ 36 | function fetchOpenRouterModels() { 37 | return new Promise((resolve) => { 38 | const options = { 39 | hostname: 'openrouter.ai', 40 | path: '/api/v1/models', 41 | method: 'GET', 42 | headers: { 43 | Accept: 'application/json' 44 | } 45 | }; 46 | 47 | const req = https.request(options, (res) => { 48 | let data = ''; 49 | res.on('data', (chunk) => { 50 | data += chunk; 51 | }); 52 | res.on('end', () => { 53 | if (res.statusCode === 200) { 54 | try { 55 | const parsedData = JSON.parse(data); 56 | resolve(parsedData.data || []); // Return the array of models 57 | } catch (e) { 58 | console.error('Error parsing OpenRouter response:', e); 59 | resolve(null); // Indicate failure 60 | } 61 | } else { 62 | console.error( 63 | `OpenRouter API request failed with status code: ${res.statusCode}` 64 | ); 65 | resolve(null); // Indicate failure 66 | } 67 | }); 68 | }); 69 | 70 | req.on('error', (e) => { 71 | console.error('Error fetching OpenRouter models:', e); 72 | resolve(null); // Indicate failure 73 | }); 74 | req.end(); 75 | }); 76 | } 77 | 78 | /** 79 | * Fetches the list of models from Ollama instance. 80 | * @param {string} baseURL - The base URL for the Ollama API (e.g., "http://localhost:11434/api") 81 | * @returns {Promise<Array|null>} A promise that resolves with the list of model objects or null if fetch fails. 82 | */ 83 | function fetchOllamaModels(baseURL = 'http://localhost:11434/api') { 84 | return new Promise((resolve) => { 85 | try { 86 | // Parse the base URL to extract hostname, port, and base path 87 | const url = new URL(baseURL); 88 | const isHttps = url.protocol === 'https:'; 89 | const port = url.port || (isHttps ? 443 : 80); 90 | const basePath = url.pathname.endsWith('/') 91 | ? url.pathname.slice(0, -1) 92 | : url.pathname; 93 | 94 | const options = { 95 | hostname: url.hostname, 96 | port: parseInt(port, 10), 97 | path: `${basePath}/tags`, 98 | method: 'GET', 99 | headers: { 100 | Accept: 'application/json' 101 | } 102 | }; 103 | 104 | const requestLib = isHttps ? https : http; 105 | const req = requestLib.request(options, (res) => { 106 | let data = ''; 107 | res.on('data', (chunk) => { 108 | data += chunk; 109 | }); 110 | res.on('end', () => { 111 | if (res.statusCode === 200) { 112 | try { 113 | const parsedData = JSON.parse(data); 114 | resolve(parsedData.models || []); // Return the array of models 115 | } catch (e) { 116 | console.error('Error parsing Ollama response:', e); 117 | resolve(null); // Indicate failure 118 | } 119 | } else { 120 | console.error( 121 | `Ollama API request failed with status code: ${res.statusCode}` 122 | ); 123 | resolve(null); // Indicate failure 124 | } 125 | }); 126 | }); 127 | 128 | req.on('error', (e) => { 129 | console.error('Error fetching Ollama models:', e); 130 | resolve(null); // Indicate failure 131 | }); 132 | req.end(); 133 | } catch (e) { 134 | console.error('Error parsing Ollama base URL:', e); 135 | resolve(null); // Indicate failure 136 | } 137 | }); 138 | } 139 | 140 | /** 141 | * Get the current model configuration 142 | * @param {Object} [options] - Options for the operation 143 | * @param {Object} [options.session] - Session object containing environment variables (for MCP) 144 | * @param {Function} [options.mcpLog] - MCP logger object (for MCP) 145 | * @param {string} [options.projectRoot] - Project root directory 146 | * @returns {Object} RESTful response with current model configuration 147 | */ 148 | async function getModelConfiguration(options = {}) { 149 | const { mcpLog, projectRoot, session } = options; 150 | 151 | const report = (level, ...args) => { 152 | if (mcpLog && typeof mcpLog[level] === 'function') { 153 | mcpLog[level](...args); 154 | } 155 | }; 156 | 157 | if (!projectRoot) { 158 | throw new Error('Project root is required but not found.'); 159 | } 160 | 161 | // Use centralized config path finding instead of hardcoded path 162 | const configPath = findConfigPath(null, { projectRoot }); 163 | const configExists = isConfigFilePresent(projectRoot); 164 | 165 | log( 166 | 'debug', 167 | `Checking for config file using findConfigPath, found: ${configPath}` 168 | ); 169 | log( 170 | 'debug', 171 | `Checking config file using isConfigFilePresent(), exists: ${configExists}` 172 | ); 173 | 174 | if (!configExists) { 175 | throw new Error(CONFIG_MISSING_ERROR); 176 | } 177 | 178 | try { 179 | // Get current settings - these should use the config from the found path automatically 180 | const mainProvider = getMainProvider(projectRoot); 181 | const mainModelId = getMainModelId(projectRoot); 182 | const researchProvider = getResearchProvider(projectRoot); 183 | const researchModelId = getResearchModelId(projectRoot); 184 | const fallbackProvider = getFallbackProvider(projectRoot); 185 | const fallbackModelId = getFallbackModelId(projectRoot); 186 | 187 | // Check API keys 188 | const mainCliKeyOk = isApiKeySet(mainProvider, session, projectRoot); 189 | const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider, projectRoot); 190 | const researchCliKeyOk = isApiKeySet( 191 | researchProvider, 192 | session, 193 | projectRoot 194 | ); 195 | const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider, projectRoot); 196 | const fallbackCliKeyOk = fallbackProvider 197 | ? isApiKeySet(fallbackProvider, session, projectRoot) 198 | : true; 199 | const fallbackMcpKeyOk = fallbackProvider 200 | ? getMcpApiKeyStatus(fallbackProvider, projectRoot) 201 | : true; 202 | 203 | // Get available models to find detailed info 204 | const availableModels = getAvailableModels(projectRoot); 205 | 206 | // Find model details 207 | const mainModelData = availableModels.find((m) => m.id === mainModelId); 208 | const researchModelData = availableModels.find( 209 | (m) => m.id === researchModelId 210 | ); 211 | const fallbackModelData = fallbackModelId 212 | ? availableModels.find((m) => m.id === fallbackModelId) 213 | : null; 214 | 215 | // Return structured configuration data 216 | return { 217 | success: true, 218 | data: { 219 | activeModels: { 220 | main: { 221 | provider: mainProvider, 222 | modelId: mainModelId, 223 | sweScore: mainModelData?.swe_score || null, 224 | cost: mainModelData?.cost_per_1m_tokens || null, 225 | keyStatus: { 226 | cli: mainCliKeyOk, 227 | mcp: mainMcpKeyOk 228 | } 229 | }, 230 | research: { 231 | provider: researchProvider, 232 | modelId: researchModelId, 233 | sweScore: researchModelData?.swe_score || null, 234 | cost: researchModelData?.cost_per_1m_tokens || null, 235 | keyStatus: { 236 | cli: researchCliKeyOk, 237 | mcp: researchMcpKeyOk 238 | } 239 | }, 240 | fallback: fallbackProvider 241 | ? { 242 | provider: fallbackProvider, 243 | modelId: fallbackModelId, 244 | sweScore: fallbackModelData?.swe_score || null, 245 | cost: fallbackModelData?.cost_per_1m_tokens || null, 246 | keyStatus: { 247 | cli: fallbackCliKeyOk, 248 | mcp: fallbackMcpKeyOk 249 | } 250 | } 251 | : null 252 | }, 253 | message: 'Successfully retrieved current model configuration' 254 | } 255 | }; 256 | } catch (error) { 257 | report('error', `Error getting model configuration: ${error.message}`); 258 | return { 259 | success: false, 260 | error: { 261 | code: 'CONFIG_ERROR', 262 | message: error.message 263 | } 264 | }; 265 | } 266 | } 267 | 268 | /** 269 | * Get all available models not currently in use 270 | * @param {Object} [options] - Options for the operation 271 | * @param {Object} [options.session] - Session object containing environment variables (for MCP) 272 | * @param {Function} [options.mcpLog] - MCP logger object (for MCP) 273 | * @param {string} [options.projectRoot] - Project root directory 274 | * @returns {Object} RESTful response with available models 275 | */ 276 | async function getAvailableModelsList(options = {}) { 277 | const { mcpLog, projectRoot } = options; 278 | 279 | const report = (level, ...args) => { 280 | if (mcpLog && typeof mcpLog[level] === 'function') { 281 | mcpLog[level](...args); 282 | } 283 | }; 284 | 285 | if (!projectRoot) { 286 | throw new Error('Project root is required but not found.'); 287 | } 288 | 289 | // Use centralized config path finding instead of hardcoded path 290 | const configPath = findConfigPath(null, { projectRoot }); 291 | const configExists = isConfigFilePresent(projectRoot); 292 | 293 | log( 294 | 'debug', 295 | `Checking for config file using findConfigPath, found: ${configPath}` 296 | ); 297 | log( 298 | 'debug', 299 | `Checking config file using isConfigFilePresent(), exists: ${configExists}` 300 | ); 301 | 302 | if (!configExists) { 303 | throw new Error(CONFIG_MISSING_ERROR); 304 | } 305 | 306 | try { 307 | // Get all available models 308 | const allAvailableModels = getAvailableModels(projectRoot); 309 | 310 | if (!allAvailableModels || allAvailableModels.length === 0) { 311 | return { 312 | success: true, 313 | data: { 314 | models: [], 315 | message: 'No available models found' 316 | } 317 | }; 318 | } 319 | 320 | // Get currently used model IDs 321 | const mainModelId = getMainModelId(projectRoot); 322 | const researchModelId = getResearchModelId(projectRoot); 323 | const fallbackModelId = getFallbackModelId(projectRoot); 324 | 325 | // Filter out placeholder models and active models 326 | const activeIds = [mainModelId, researchModelId, fallbackModelId].filter( 327 | Boolean 328 | ); 329 | const otherAvailableModels = allAvailableModels.map((model) => ({ 330 | provider: model.provider || 'N/A', 331 | modelId: model.id, 332 | sweScore: model.swe_score || null, 333 | cost: model.cost_per_1m_tokens || null, 334 | allowedRoles: model.allowed_roles || [] 335 | })); 336 | 337 | return { 338 | success: true, 339 | data: { 340 | models: otherAvailableModels, 341 | message: `Successfully retrieved ${otherAvailableModels.length} available models` 342 | } 343 | }; 344 | } catch (error) { 345 | report('error', `Error getting available models: ${error.message}`); 346 | return { 347 | success: false, 348 | error: { 349 | code: 'MODELS_LIST_ERROR', 350 | message: error.message 351 | } 352 | }; 353 | } 354 | } 355 | 356 | /** 357 | * Update a specific model in the configuration 358 | * @param {string} role - The model role to update ('main', 'research', 'fallback') 359 | * @param {string} modelId - The model ID to set for the role 360 | * @param {Object} [options] - Options for the operation 361 | * @param {string} [options.providerHint] - Provider hint if already determined ('openrouter' or 'ollama') 362 | * @param {Object} [options.session] - Session object containing environment variables (for MCP) 363 | * @param {Function} [options.mcpLog] - MCP logger object (for MCP) 364 | * @param {string} [options.projectRoot] - Project root directory 365 | * @returns {Object} RESTful response with result of update operation 366 | */ 367 | async function setModel(role, modelId, options = {}) { 368 | const { mcpLog, projectRoot, providerHint } = options; 369 | 370 | const report = (level, ...args) => { 371 | if (mcpLog && typeof mcpLog[level] === 'function') { 372 | mcpLog[level](...args); 373 | } 374 | }; 375 | 376 | if (!projectRoot) { 377 | throw new Error('Project root is required but not found.'); 378 | } 379 | 380 | // Use centralized config path finding instead of hardcoded path 381 | const configPath = findConfigPath(null, { projectRoot }); 382 | const configExists = isConfigFilePresent(projectRoot); 383 | 384 | log( 385 | 'debug', 386 | `Checking for config file using findConfigPath, found: ${configPath}` 387 | ); 388 | log( 389 | 'debug', 390 | `Checking config file using isConfigFilePresent(), exists: ${configExists}` 391 | ); 392 | 393 | if (!configExists) { 394 | throw new Error(CONFIG_MISSING_ERROR); 395 | } 396 | 397 | // Validate role 398 | if (!['main', 'research', 'fallback'].includes(role)) { 399 | return { 400 | success: false, 401 | error: { 402 | code: 'INVALID_ROLE', 403 | message: `Invalid role: ${role}. Must be one of: main, research, fallback.` 404 | } 405 | }; 406 | } 407 | 408 | // Validate model ID 409 | if (typeof modelId !== 'string' || modelId.trim() === '') { 410 | return { 411 | success: false, 412 | error: { 413 | code: 'INVALID_MODEL_ID', 414 | message: `Invalid model ID: ${modelId}. Must be a non-empty string.` 415 | } 416 | }; 417 | } 418 | 419 | try { 420 | const availableModels = getAvailableModels(projectRoot); 421 | const currentConfig = getConfig(projectRoot); 422 | let determinedProvider = null; // Initialize provider 423 | let warningMessage = null; 424 | 425 | // Find the model data in internal list initially to see if it exists at all 426 | let modelData = availableModels.find((m) => m.id === modelId); 427 | 428 | // --- Revised Logic: Prioritize providerHint --- // 429 | 430 | if (providerHint) { 431 | // Hint provided (--ollama or --openrouter flag used) 432 | if (modelData && modelData.provider === providerHint) { 433 | // Found internally AND provider matches the hint 434 | determinedProvider = providerHint; 435 | report( 436 | 'info', 437 | `Model ${modelId} found internally with matching provider hint ${determinedProvider}.` 438 | ); 439 | } else { 440 | // Either not found internally, OR found but under a DIFFERENT provider than hinted. 441 | // Proceed with custom logic based ONLY on the hint. 442 | if (providerHint === CUSTOM_PROVIDERS.OPENROUTER) { 443 | // Check OpenRouter ONLY because hint was openrouter 444 | report('info', `Checking OpenRouter for ${modelId} (as hinted)...`); 445 | const openRouterModels = await fetchOpenRouterModels(); 446 | 447 | if ( 448 | openRouterModels && 449 | openRouterModels.some((m) => m.id === modelId) 450 | ) { 451 | determinedProvider = CUSTOM_PROVIDERS.OPENROUTER; 452 | 453 | // Check if this is a free model (ends with :free) 454 | if (modelId.endsWith(':free')) { 455 | warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(':free', '')}' for full functionality.`; 456 | } else { 457 | warningMessage = `Warning: Custom OpenRouter model '${modelId}' set. This model is not officially validated by Taskmaster and may not function as expected.`; 458 | } 459 | 460 | report('warn', warningMessage); 461 | } else { 462 | // Hinted as OpenRouter but not found in live check 463 | throw new Error( 464 | `Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.` 465 | ); 466 | } 467 | } else if (providerHint === CUSTOM_PROVIDERS.OLLAMA) { 468 | // Check Ollama ONLY because hint was ollama 469 | report('info', `Checking Ollama for ${modelId} (as hinted)...`); 470 | 471 | // Get the Ollama base URL from config 472 | const ollamaBaseURL = getBaseUrlForRole(role, projectRoot); 473 | const ollamaModels = await fetchOllamaModels(ollamaBaseURL); 474 | 475 | if (ollamaModels === null) { 476 | // Connection failed - server probably not running 477 | throw new Error( 478 | `Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.` 479 | ); 480 | } else if (ollamaModels.some((m) => m.model === modelId)) { 481 | determinedProvider = CUSTOM_PROVIDERS.OLLAMA; 482 | warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`; 483 | report('warn', warningMessage); 484 | } else { 485 | // Server is running but model not found 486 | const tagsUrl = `${ollamaBaseURL}/tags`; 487 | throw new Error( 488 | `Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}` 489 | ); 490 | } 491 | } else if (providerHint === CUSTOM_PROVIDERS.BEDROCK) { 492 | // Set provider without model validation since Bedrock models are managed by AWS 493 | determinedProvider = CUSTOM_PROVIDERS.BEDROCK; 494 | warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`; 495 | report('warn', warningMessage); 496 | } else if (providerHint === CUSTOM_PROVIDERS.CLAUDE_CODE) { 497 | // Claude Code provider - check if model exists in our list 498 | determinedProvider = CUSTOM_PROVIDERS.CLAUDE_CODE; 499 | // Re-find modelData specifically for claude-code provider 500 | const claudeCodeModels = availableModels.filter( 501 | (m) => m.provider === 'claude-code' 502 | ); 503 | const claudeCodeModelData = claudeCodeModels.find( 504 | (m) => m.id === modelId 505 | ); 506 | if (claudeCodeModelData) { 507 | // Update modelData to the found claude-code model 508 | modelData = claudeCodeModelData; 509 | report('info', `Setting Claude Code model '${modelId}'.`); 510 | } else { 511 | warningMessage = `Warning: Claude Code model '${modelId}' not found in supported models. Setting without validation.`; 512 | report('warn', warningMessage); 513 | } 514 | } else if (providerHint === CUSTOM_PROVIDERS.AZURE) { 515 | // Set provider without model validation since Azure models are managed by Azure 516 | determinedProvider = CUSTOM_PROVIDERS.AZURE; 517 | warningMessage = `Warning: Custom Azure model '${modelId}' set. Please ensure the model deployment is valid and accessible in your Azure account.`; 518 | report('warn', warningMessage); 519 | } else if (providerHint === CUSTOM_PROVIDERS.VERTEX) { 520 | // Set provider without model validation since Vertex models are managed by Google Cloud 521 | determinedProvider = CUSTOM_PROVIDERS.VERTEX; 522 | warningMessage = `Warning: Custom Vertex AI model '${modelId}' set. Please ensure the model is valid and accessible in your Google Cloud project.`; 523 | report('warn', warningMessage); 524 | } else if (providerHint === CUSTOM_PROVIDERS.GEMINI_CLI) { 525 | // Gemini CLI provider - check if model exists in our list 526 | determinedProvider = CUSTOM_PROVIDERS.GEMINI_CLI; 527 | // Re-find modelData specifically for gemini-cli provider 528 | const geminiCliModels = availableModels.filter( 529 | (m) => m.provider === 'gemini-cli' 530 | ); 531 | const geminiCliModelData = geminiCliModels.find( 532 | (m) => m.id === modelId 533 | ); 534 | if (geminiCliModelData) { 535 | // Update modelData to the found gemini-cli model 536 | modelData = geminiCliModelData; 537 | report('info', `Setting Gemini CLI model '${modelId}'.`); 538 | } else { 539 | warningMessage = `Warning: Gemini CLI model '${modelId}' not found in supported models. Setting without validation.`; 540 | report('warn', warningMessage); 541 | } 542 | } else { 543 | // Invalid provider hint - should not happen with our constants 544 | throw new Error(`Invalid provider hint received: ${providerHint}`); 545 | } 546 | } 547 | } else { 548 | // No hint provided (flags not used) 549 | if (modelData) { 550 | // Found internally, use the provider from the internal list 551 | determinedProvider = modelData.provider; 552 | report( 553 | 'info', 554 | `Model ${modelId} found internally with provider ${determinedProvider}.` 555 | ); 556 | } else { 557 | // Model not found and no provider hint was given 558 | return { 559 | success: false, 560 | error: { 561 | code: 'MODEL_NOT_FOUND_NO_HINT', 562 | message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter, --ollama, --bedrock, --azure, or --vertex.` 563 | } 564 | }; 565 | } 566 | } 567 | 568 | // --- End of Revised Logic --- // 569 | 570 | // At this point, we should have a determinedProvider if the model is valid (internally or custom) 571 | if (!determinedProvider) { 572 | // This case acts as a safeguard 573 | return { 574 | success: false, 575 | error: { 576 | code: 'PROVIDER_UNDETERMINED', 577 | message: `Could not determine the provider for model ID "${modelId}".` 578 | } 579 | }; 580 | } 581 | 582 | // Update configuration 583 | currentConfig.models[role] = { 584 | ...currentConfig.models[role], // Keep existing params like temperature 585 | provider: determinedProvider, 586 | modelId: modelId 587 | }; 588 | 589 | // If model data is available, update maxTokens from supported-models.json 590 | if (modelData && modelData.max_tokens) { 591 | currentConfig.models[role].maxTokens = modelData.max_tokens; 592 | } 593 | 594 | // Write updated configuration 595 | const writeResult = writeConfig(currentConfig, projectRoot); 596 | if (!writeResult) { 597 | return { 598 | success: false, 599 | error: { 600 | code: 'CONFIG_WRITE_ERROR', 601 | message: 'Error writing updated configuration to configuration file' 602 | } 603 | }; 604 | } 605 | 606 | const successMessage = `Successfully set ${role} model to ${modelId} (Provider: ${determinedProvider})`; 607 | report('info', successMessage); 608 | 609 | return { 610 | success: true, 611 | data: { 612 | role, 613 | provider: determinedProvider, 614 | modelId, 615 | message: successMessage, 616 | warning: warningMessage // Include warning in the response data 617 | } 618 | }; 619 | } catch (error) { 620 | report('error', `Error setting ${role} model: ${error.message}`); 621 | return { 622 | success: false, 623 | error: { 624 | code: 'SET_MODEL_ERROR', 625 | message: error.message 626 | } 627 | }; 628 | } 629 | } 630 | 631 | /** 632 | * Get API key status for all known providers. 633 | * @param {Object} [options] - Options for the operation 634 | * @param {Object} [options.session] - Session object containing environment variables (for MCP) 635 | * @param {Function} [options.mcpLog] - MCP logger object (for MCP) 636 | * @param {string} [options.projectRoot] - Project root directory 637 | * @returns {Object} RESTful response with API key status report 638 | */ 639 | async function getApiKeyStatusReport(options = {}) { 640 | const { mcpLog, projectRoot, session } = options; 641 | const report = (level, ...args) => { 642 | if (mcpLog && typeof mcpLog[level] === 'function') { 643 | mcpLog[level](...args); 644 | } 645 | }; 646 | 647 | try { 648 | const providers = getAllProviders(); 649 | const providersToCheck = providers.filter( 650 | (p) => p.toLowerCase() !== 'ollama' 651 | ); // Ollama is not a provider, it's a service, doesn't need an api key usually 652 | const statusReport = providersToCheck.map((provider) => { 653 | // Use provided projectRoot for MCP status check 654 | const cliOk = isApiKeySet(provider, session, projectRoot); // Pass session and projectRoot for CLI check 655 | const mcpOk = getMcpApiKeyStatus(provider, projectRoot); 656 | return { 657 | provider, 658 | cli: cliOk, 659 | mcp: mcpOk 660 | }; 661 | }); 662 | 663 | report('info', 'Successfully generated API key status report.'); 664 | return { 665 | success: true, 666 | data: { 667 | report: statusReport, 668 | message: 'API key status report generated.' 669 | } 670 | }; 671 | } catch (error) { 672 | report('error', `Error generating API key status report: ${error.message}`); 673 | return { 674 | success: false, 675 | error: { 676 | code: 'API_KEY_STATUS_ERROR', 677 | message: error.message 678 | } 679 | }; 680 | } 681 | } 682 | 683 | export { 684 | getModelConfiguration, 685 | getAvailableModelsList, 686 | setModel, 687 | getApiKeyStatusReport 688 | }; 689 | ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/add-task.js: -------------------------------------------------------------------------------- ```javascript 1 | import path from 'path'; 2 | import chalk from 'chalk'; 3 | import boxen from 'boxen'; 4 | import Table from 'cli-table3'; 5 | import { z } from 'zod'; 6 | import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search 7 | 8 | import { 9 | displayBanner, 10 | getStatusWithColor, 11 | startLoadingIndicator, 12 | stopLoadingIndicator, 13 | succeedLoadingIndicator, 14 | failLoadingIndicator, 15 | displayAiUsageSummary, 16 | displayContextAnalysis 17 | } from '../ui.js'; 18 | import { 19 | readJSON, 20 | writeJSON, 21 | log as consoleLog, 22 | truncate, 23 | ensureTagMetadata, 24 | performCompleteTagMigration, 25 | markMigrationForNotice 26 | } from '../utils.js'; 27 | import { generateObjectService } from '../ai-services-unified.js'; 28 | import { getDefaultPriority, hasCodebaseAnalysis } from '../config-manager.js'; 29 | import { getPromptManager } from '../prompt-manager.js'; 30 | import ContextGatherer from '../utils/contextGatherer.js'; 31 | import generateTaskFiles from './generate-task-files.js'; 32 | import { 33 | TASK_PRIORITY_OPTIONS, 34 | DEFAULT_TASK_PRIORITY, 35 | isValidTaskPriority, 36 | normalizeTaskPriority 37 | } from '../../../src/constants/task-priority.js'; 38 | 39 | // Define Zod schema for the expected AI output object 40 | const AiTaskDataSchema = z.object({ 41 | title: z.string().describe('Clear, concise title for the task'), 42 | description: z 43 | .string() 44 | .describe('A one or two sentence description of the task'), 45 | details: z 46 | .string() 47 | .describe('In-depth implementation details, considerations, and guidance'), 48 | testStrategy: z 49 | .string() 50 | .describe('Detailed approach for verifying task completion'), 51 | dependencies: z 52 | .array(z.number()) 53 | .nullable() 54 | .describe( 55 | 'Array of task IDs that this task depends on (must be completed before this task can start)' 56 | ) 57 | }); 58 | 59 | /** 60 | * Get all tasks from all tags 61 | * @param {Object} rawData - The raw tagged data object 62 | * @returns {Array} A flat array of all task objects 63 | */ 64 | function getAllTasks(rawData) { 65 | let allTasks = []; 66 | for (const tagName in rawData) { 67 | if ( 68 | Object.prototype.hasOwnProperty.call(rawData, tagName) && 69 | rawData[tagName] && 70 | Array.isArray(rawData[tagName].tasks) 71 | ) { 72 | allTasks = allTasks.concat(rawData[tagName].tasks); 73 | } 74 | } 75 | return allTasks; 76 | } 77 | 78 | /** 79 | * Add a new task using AI 80 | * @param {string} tasksPath - Path to the tasks.json file 81 | * @param {string} prompt - Description of the task to add (required for AI-driven creation) 82 | * @param {Array} dependencies - Task dependencies 83 | * @param {string} priority - Task priority 84 | * @param {function} reportProgress - Function to report progress to MCP server (optional) 85 | * @param {Object} mcpLog - MCP logger object (optional) 86 | * @param {Object} session - Session object from MCP server (optional) 87 | * @param {string} outputFormat - Output format (text or json) 88 | * @param {Object} customEnv - Custom environment variables (optional) - Note: AI params override deprecated 89 | * @param {Object} manualTaskData - Manual task data (optional, for direct task creation without AI) 90 | * @param {boolean} useResearch - Whether to use the research model (passed to unified service) 91 | * @param {Object} context - Context object containing session and potentially projectRoot 92 | * @param {string} [context.projectRoot] - Project root path (for MCP/env fallback) 93 | * @param {string} [context.commandName] - The name of the command being executed (for telemetry) 94 | * @param {string} [context.outputType] - The output type ('cli' or 'mcp', for telemetry) 95 | * @param {string} [context.tag] - Tag for the task (optional) 96 | * @returns {Promise<object>} An object containing newTaskId and telemetryData 97 | */ 98 | async function addTask( 99 | tasksPath, 100 | prompt, 101 | dependencies = [], 102 | priority = null, 103 | context = {}, 104 | outputFormat = 'text', // Default to text for CLI 105 | manualTaskData = null, 106 | useResearch = false 107 | ) { 108 | const { session, mcpLog, projectRoot, commandName, outputType, tag } = 109 | context; 110 | const isMCP = !!mcpLog; 111 | 112 | // Create a consistent logFn object regardless of context 113 | const logFn = isMCP 114 | ? mcpLog // Use MCP logger if provided 115 | : { 116 | // Create a wrapper around consoleLog for CLI 117 | info: (...args) => consoleLog('info', ...args), 118 | warn: (...args) => consoleLog('warn', ...args), 119 | error: (...args) => consoleLog('error', ...args), 120 | debug: (...args) => consoleLog('debug', ...args), 121 | success: (...args) => consoleLog('success', ...args) 122 | }; 123 | 124 | // Validate priority - only accept high, medium, or low 125 | let effectivePriority = 126 | priority || getDefaultPriority(projectRoot) || DEFAULT_TASK_PRIORITY; 127 | 128 | // If priority is provided, validate and normalize it 129 | if (priority) { 130 | const normalizedPriority = normalizeTaskPriority(priority); 131 | if (normalizedPriority) { 132 | effectivePriority = normalizedPriority; 133 | } else { 134 | if (outputFormat === 'text') { 135 | consoleLog( 136 | 'warn', 137 | `Invalid priority "${priority}". Using default priority "${DEFAULT_TASK_PRIORITY}".` 138 | ); 139 | } 140 | effectivePriority = DEFAULT_TASK_PRIORITY; 141 | } 142 | } 143 | 144 | logFn.info( 145 | `Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}` 146 | ); 147 | if (tag) { 148 | logFn.info(`Using tag context: ${tag}`); 149 | } 150 | 151 | let loadingIndicator = null; 152 | let aiServiceResponse = null; // To store the full response from AI service 153 | 154 | // Create custom reporter that checks for MCP log 155 | const report = (message, level = 'info') => { 156 | if (mcpLog) { 157 | mcpLog[level](message); 158 | } else if (outputFormat === 'text') { 159 | consoleLog(level, message); 160 | } 161 | }; 162 | 163 | /** 164 | * Recursively builds a dependency graph for a given task 165 | * @param {Array} tasks - All tasks from tasks.json 166 | * @param {number} taskId - ID of the task to analyze 167 | * @param {Set} visited - Set of already visited task IDs 168 | * @param {Map} depthMap - Map of task ID to its depth in the graph 169 | * @param {number} depth - Current depth in the recursion 170 | * @return {Object} Dependency graph data 171 | */ 172 | function buildDependencyGraph( 173 | tasks, 174 | taskId, 175 | visited = new Set(), 176 | depthMap = new Map(), 177 | depth = 0 178 | ) { 179 | // Skip if we've already visited this task or it doesn't exist 180 | if (visited.has(taskId)) { 181 | return null; 182 | } 183 | 184 | // Find the task 185 | const task = tasks.find((t) => t.id === taskId); 186 | if (!task) { 187 | return null; 188 | } 189 | 190 | // Mark as visited 191 | visited.add(taskId); 192 | 193 | // Update depth if this is a deeper path to this task 194 | if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) { 195 | depthMap.set(taskId, depth); 196 | } 197 | 198 | // Process dependencies 199 | const dependencyData = []; 200 | if (task.dependencies && task.dependencies.length > 0) { 201 | for (const depId of task.dependencies) { 202 | const depData = buildDependencyGraph( 203 | tasks, 204 | depId, 205 | visited, 206 | depthMap, 207 | depth + 1 208 | ); 209 | if (depData) { 210 | dependencyData.push(depData); 211 | } 212 | } 213 | } 214 | 215 | return { 216 | id: task.id, 217 | title: task.title, 218 | description: task.description, 219 | status: task.status, 220 | dependencies: dependencyData 221 | }; 222 | } 223 | 224 | try { 225 | // Read the existing tasks - IMPORTANT: Read the raw data without tag resolution 226 | let rawData = readJSON(tasksPath, projectRoot, tag); // No tag parameter 227 | 228 | // Handle the case where readJSON returns resolved data with _rawTaggedData 229 | if (rawData && rawData._rawTaggedData) { 230 | // Use the raw tagged data and discard the resolved view 231 | rawData = rawData._rawTaggedData; 232 | } 233 | 234 | // If file doesn't exist or is invalid, create a new structure in memory 235 | if (!rawData) { 236 | report( 237 | 'tasks.json not found or invalid. Initializing new structure.', 238 | 'info' 239 | ); 240 | rawData = { 241 | master: { 242 | tasks: [], 243 | metadata: { 244 | created: new Date().toISOString(), 245 | description: 'Default tasks context' 246 | } 247 | } 248 | }; 249 | // Do not write the file here; it will be written later with the new task. 250 | } 251 | 252 | // Handle legacy format migration using utilities 253 | if (rawData && Array.isArray(rawData.tasks) && !rawData._rawTaggedData) { 254 | report('Legacy format detected. Migrating to tagged format...', 'info'); 255 | 256 | // This is legacy format - migrate it to tagged format 257 | rawData = { 258 | master: { 259 | tasks: rawData.tasks, 260 | metadata: rawData.metadata || { 261 | created: new Date().toISOString(), 262 | updated: new Date().toISOString(), 263 | description: 'Tasks for master context' 264 | } 265 | } 266 | }; 267 | // Ensure proper metadata using utility 268 | ensureTagMetadata(rawData.master, { 269 | description: 'Tasks for master context' 270 | }); 271 | // Do not write the file here; it will be written later with the new task. 272 | 273 | // Perform complete migration (config.json, state.json) 274 | performCompleteTagMigration(tasksPath); 275 | markMigrationForNotice(tasksPath); 276 | 277 | report('Successfully migrated to tagged format.', 'success'); 278 | } 279 | 280 | // Use the provided tag, or the current active tag, or default to 'master' 281 | const targetTag = tag; 282 | 283 | // Ensure the target tag exists 284 | if (!rawData[targetTag]) { 285 | report( 286 | `Tag "${targetTag}" does not exist. Please create it first using the 'add-tag' command.`, 287 | 'error' 288 | ); 289 | throw new Error(`Tag "${targetTag}" not found.`); 290 | } 291 | 292 | // Ensure the target tag has a tasks array and metadata object 293 | if (!rawData[targetTag].tasks) { 294 | rawData[targetTag].tasks = []; 295 | } 296 | if (!rawData[targetTag].metadata) { 297 | rawData[targetTag].metadata = { 298 | created: new Date().toISOString(), 299 | updated: new Date().toISOString(), 300 | description: `` 301 | }; 302 | } 303 | 304 | // Get a flat list of ALL tasks across ALL tags to validate dependencies 305 | const allTasks = getAllTasks(rawData); 306 | 307 | // Find the highest task ID *within the target tag* to determine the next ID 308 | const tasksInTargetTag = rawData[targetTag].tasks; 309 | const highestId = 310 | tasksInTargetTag.length > 0 311 | ? Math.max(...tasksInTargetTag.map((t) => t.id)) 312 | : 0; 313 | const newTaskId = highestId + 1; 314 | 315 | // Only show UI box for CLI mode 316 | if (outputFormat === 'text') { 317 | console.log( 318 | boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), { 319 | padding: 1, 320 | borderColor: 'blue', 321 | borderStyle: 'round', 322 | margin: { top: 1, bottom: 1 } 323 | }) 324 | ); 325 | } 326 | 327 | // Validate dependencies before proceeding 328 | const invalidDeps = dependencies.filter((depId) => { 329 | // Ensure depId is parsed as a number for comparison 330 | const numDepId = parseInt(depId, 10); 331 | return Number.isNaN(numDepId) || !allTasks.some((t) => t.id === numDepId); 332 | }); 333 | 334 | if (invalidDeps.length > 0) { 335 | report( 336 | `The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`, 337 | 'warn' 338 | ); 339 | report('Removing invalid dependencies...', 'info'); 340 | dependencies = dependencies.filter( 341 | (depId) => !invalidDeps.includes(depId) 342 | ); 343 | } 344 | // Ensure dependencies are numbers 345 | const numericDependencies = dependencies.map((dep) => parseInt(dep, 10)); 346 | 347 | // Build dependency graphs for explicitly specified dependencies 348 | const dependencyGraphs = []; 349 | const allRelatedTaskIds = new Set(); 350 | const depthMap = new Map(); 351 | 352 | // First pass: build a complete dependency graph for each specified dependency 353 | for (const depId of numericDependencies) { 354 | const graph = buildDependencyGraph(allTasks, depId, new Set(), depthMap); 355 | if (graph) { 356 | dependencyGraphs.push(graph); 357 | } 358 | } 359 | 360 | // Second pass: build a set of all related task IDs for flat analysis 361 | for (const [taskId, depth] of depthMap.entries()) { 362 | allRelatedTaskIds.add(taskId); 363 | } 364 | 365 | let taskData; 366 | 367 | // Check if manual task data is provided 368 | if (manualTaskData) { 369 | report('Using manually provided task data', 'info'); 370 | taskData = manualTaskData; 371 | report('DEBUG: Taking MANUAL task data path.', 'debug'); 372 | 373 | // Basic validation for manual data 374 | if ( 375 | !taskData.title || 376 | typeof taskData.title !== 'string' || 377 | !taskData.description || 378 | typeof taskData.description !== 'string' 379 | ) { 380 | throw new Error( 381 | 'Manual task data must include at least a title and description.' 382 | ); 383 | } 384 | } else { 385 | report('DEBUG: Taking AI task generation path.', 'debug'); 386 | // --- Refactored AI Interaction --- 387 | report(`Generating task data with AI with prompt:\n${prompt}`, 'info'); 388 | 389 | // --- Use the new ContextGatherer --- 390 | const contextGatherer = new ContextGatherer(projectRoot, tag); 391 | const gatherResult = await contextGatherer.gather({ 392 | semanticQuery: prompt, 393 | dependencyTasks: numericDependencies, 394 | format: 'research' 395 | }); 396 | 397 | const gatheredContext = gatherResult.context; 398 | const analysisData = gatherResult.analysisData; 399 | 400 | // Display context analysis if not in silent mode 401 | if (outputFormat === 'text' && analysisData) { 402 | displayContextAnalysis(analysisData, prompt, gatheredContext.length); 403 | } 404 | 405 | // Add any manually provided details to the prompt for context 406 | let contextFromArgs = ''; 407 | if (manualTaskData?.title) 408 | contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`; 409 | if (manualTaskData?.description) 410 | contextFromArgs += `\n- Suggested Description: "${manualTaskData.description}"`; 411 | if (manualTaskData?.details) 412 | contextFromArgs += `\n- Additional Details Context: "${manualTaskData.details}"`; 413 | if (manualTaskData?.testStrategy) 414 | contextFromArgs += `\n- Additional Test Strategy Context: "${manualTaskData.testStrategy}"`; 415 | 416 | // Load prompts using PromptManager 417 | const promptManager = getPromptManager(); 418 | const { systemPrompt, userPrompt } = await promptManager.loadPrompt( 419 | 'add-task', 420 | { 421 | prompt, 422 | newTaskId, 423 | existingTasks: allTasks, 424 | gatheredContext, 425 | contextFromArgs, 426 | useResearch, 427 | priority: effectivePriority, 428 | dependencies: numericDependencies, 429 | hasCodebaseAnalysis: hasCodebaseAnalysis( 430 | useResearch, 431 | projectRoot, 432 | session 433 | ), 434 | projectRoot: projectRoot 435 | } 436 | ); 437 | 438 | // Start the loading indicator - only for text mode 439 | if (outputFormat === 'text') { 440 | loadingIndicator = startLoadingIndicator( 441 | `Generating new task with ${useResearch ? 'Research' : 'Main'} AI... \n` 442 | ); 443 | } 444 | 445 | try { 446 | const serviceRole = useResearch ? 'research' : 'main'; 447 | report('DEBUG: Calling generateObjectService...', 'debug'); 448 | 449 | aiServiceResponse = await generateObjectService({ 450 | // Capture the full response 451 | role: serviceRole, 452 | session: session, 453 | projectRoot: projectRoot, 454 | schema: AiTaskDataSchema, 455 | objectName: 'newTaskData', 456 | systemPrompt: systemPrompt, 457 | prompt: userPrompt, 458 | commandName: commandName || 'add-task', // Use passed commandName or default 459 | outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive 460 | }); 461 | report('DEBUG: generateObjectService returned successfully.', 'debug'); 462 | 463 | if (!aiServiceResponse || !aiServiceResponse.mainResult) { 464 | throw new Error( 465 | 'AI service did not return the expected object structure.' 466 | ); 467 | } 468 | 469 | // Prefer mainResult if it looks like a valid task object, otherwise try mainResult.object 470 | if ( 471 | aiServiceResponse.mainResult.title && 472 | aiServiceResponse.mainResult.description 473 | ) { 474 | taskData = aiServiceResponse.mainResult; 475 | } else if ( 476 | aiServiceResponse.mainResult.object && 477 | aiServiceResponse.mainResult.object.title && 478 | aiServiceResponse.mainResult.object.description 479 | ) { 480 | taskData = aiServiceResponse.mainResult.object; 481 | } else { 482 | throw new Error('AI service did not return a valid task object.'); 483 | } 484 | 485 | report('Successfully generated task data from AI.', 'success'); 486 | 487 | // Success! Show checkmark 488 | if (loadingIndicator) { 489 | succeedLoadingIndicator( 490 | loadingIndicator, 491 | 'Task generated successfully' 492 | ); 493 | loadingIndicator = null; // Clear it 494 | } 495 | } catch (error) { 496 | // Failure! Show X 497 | if (loadingIndicator) { 498 | failLoadingIndicator(loadingIndicator, 'AI generation failed'); 499 | loadingIndicator = null; 500 | } 501 | report( 502 | `DEBUG: generateObjectService caught error: ${error.message}`, 503 | 'debug' 504 | ); 505 | report(`Error generating task with AI: ${error.message}`, 'error'); 506 | throw error; // Re-throw error after logging 507 | } finally { 508 | report('DEBUG: generateObjectService finally block reached.', 'debug'); 509 | // Clean up if somehow still running 510 | if (loadingIndicator) { 511 | stopLoadingIndicator(loadingIndicator); 512 | } 513 | } 514 | // --- End Refactored AI Interaction --- 515 | } 516 | 517 | // Create the new task object 518 | const newTask = { 519 | id: newTaskId, 520 | title: taskData.title, 521 | description: taskData.description, 522 | details: taskData.details || '', 523 | testStrategy: taskData.testStrategy || '', 524 | status: 'pending', 525 | dependencies: taskData.dependencies?.length 526 | ? taskData.dependencies 527 | : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified 528 | priority: effectivePriority, 529 | subtasks: [] // Initialize with empty subtasks array 530 | }; 531 | 532 | // Additional check: validate all dependencies in the AI response 533 | if (taskData.dependencies?.length) { 534 | const allValidDeps = taskData.dependencies.every((depId) => { 535 | const numDepId = parseInt(depId, 10); 536 | return ( 537 | !Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId) 538 | ); 539 | }); 540 | 541 | if (!allValidDeps) { 542 | report( 543 | 'AI suggested invalid dependencies. Filtering them out...', 544 | 'warn' 545 | ); 546 | newTask.dependencies = taskData.dependencies.filter((depId) => { 547 | const numDepId = parseInt(depId, 10); 548 | return ( 549 | !Number.isNaN(numDepId) && allTasks.some((t) => t.id === numDepId) 550 | ); 551 | }); 552 | } 553 | } 554 | 555 | // Add the task to the tasks array OF THE CORRECT TAG 556 | rawData[targetTag].tasks.push(newTask); 557 | // Update the tag's metadata 558 | ensureTagMetadata(rawData[targetTag], { 559 | description: `Tasks for ${targetTag} context` 560 | }); 561 | 562 | report('DEBUG: Writing tasks.json...', 'debug'); 563 | // Write the updated raw data back to the file 564 | // The writeJSON function will automatically filter out _rawTaggedData 565 | writeJSON(tasksPath, rawData, projectRoot, targetTag); 566 | report('DEBUG: tasks.json written.', 'debug'); 567 | 568 | // Show success message - only for text output (CLI) 569 | if (outputFormat === 'text') { 570 | const table = new Table({ 571 | head: [ 572 | chalk.cyan.bold('ID'), 573 | chalk.cyan.bold('Title'), 574 | chalk.cyan.bold('Description') 575 | ], 576 | colWidths: [5, 30, 50] // Adjust widths as needed 577 | }); 578 | 579 | table.push([ 580 | newTask.id, 581 | truncate(newTask.title, 27), 582 | truncate(newTask.description, 47) 583 | ]); 584 | 585 | console.log(chalk.green('✓ New task created successfully:')); 586 | console.log(table.toString()); 587 | 588 | // Helper to get priority color 589 | const getPriorityColor = (p) => { 590 | switch (p?.toLowerCase()) { 591 | case 'high': 592 | return 'red'; 593 | case 'low': 594 | return 'gray'; 595 | default: 596 | return 'yellow'; 597 | } 598 | }; 599 | 600 | // Check if AI added new dependencies that weren't explicitly provided 601 | const aiAddedDeps = newTask.dependencies.filter( 602 | (dep) => !numericDependencies.includes(dep) 603 | ); 604 | 605 | // Check if AI removed any dependencies that were explicitly provided 606 | const aiRemovedDeps = numericDependencies.filter( 607 | (dep) => !newTask.dependencies.includes(dep) 608 | ); 609 | 610 | // Get task titles for dependencies to display 611 | const depTitles = {}; 612 | newTask.dependencies.forEach((dep) => { 613 | const depTask = allTasks.find((t) => t.id === dep); 614 | if (depTask) { 615 | depTitles[dep] = truncate(depTask.title, 30); 616 | } 617 | }); 618 | 619 | // Prepare dependency display string 620 | let dependencyDisplay = ''; 621 | if (newTask.dependencies.length > 0) { 622 | dependencyDisplay = chalk.white('Dependencies:') + '\n'; 623 | newTask.dependencies.forEach((dep) => { 624 | const isAiAdded = aiAddedDeps.includes(dep); 625 | const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : ''; 626 | dependencyDisplay += 627 | chalk.white( 628 | ` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}` 629 | ) + '\n'; 630 | }); 631 | } else { 632 | dependencyDisplay = chalk.white('Dependencies: None') + '\n'; 633 | } 634 | 635 | // Add info about removed dependencies if any 636 | if (aiRemovedDeps.length > 0) { 637 | dependencyDisplay += 638 | chalk.gray('\nUser-specified dependencies that were not used:') + 639 | '\n'; 640 | aiRemovedDeps.forEach((dep) => { 641 | const depTask = allTasks.find((t) => t.id === dep); 642 | const title = depTask ? truncate(depTask.title, 30) : 'Unknown task'; 643 | dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n'; 644 | }); 645 | } 646 | 647 | // Add dependency analysis summary 648 | let dependencyAnalysis = ''; 649 | if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) { 650 | dependencyAnalysis = 651 | '\n' + chalk.white.bold('Dependency Analysis:') + '\n'; 652 | if (aiAddedDeps.length > 0) { 653 | dependencyAnalysis += 654 | chalk.green( 655 | `AI identified ${aiAddedDeps.length} additional dependencies` 656 | ) + '\n'; 657 | } 658 | if (aiRemovedDeps.length > 0) { 659 | dependencyAnalysis += 660 | chalk.yellow( 661 | `AI excluded ${aiRemovedDeps.length} user-provided dependencies` 662 | ) + '\n'; 663 | } 664 | } 665 | 666 | // Show success message box 667 | console.log( 668 | boxen( 669 | chalk.white.bold(`Task ${newTaskId} Created Successfully`) + 670 | '\n\n' + 671 | chalk.white(`Title: ${newTask.title}`) + 672 | '\n' + 673 | chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + 674 | '\n' + 675 | chalk.white( 676 | `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` 677 | ) + 678 | '\n\n' + 679 | dependencyDisplay + 680 | dependencyAnalysis + 681 | '\n' + 682 | chalk.white.bold('Next Steps:') + 683 | '\n' + 684 | chalk.cyan( 685 | `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details` 686 | ) + 687 | '\n' + 688 | chalk.cyan( 689 | `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it` 690 | ) + 691 | '\n' + 692 | chalk.cyan( 693 | `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks` 694 | ), 695 | { padding: 1, borderColor: 'green', borderStyle: 'round' } 696 | ) 697 | ); 698 | 699 | // Display AI Usage Summary if telemetryData is available 700 | if ( 701 | aiServiceResponse && 702 | aiServiceResponse.telemetryData && 703 | (outputType === 'cli' || outputType === 'text') 704 | ) { 705 | displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); 706 | } 707 | } 708 | 709 | report( 710 | `DEBUG: Returning new task ID: ${newTaskId} and telemetry.`, 711 | 'debug' 712 | ); 713 | return { 714 | newTaskId: newTaskId, 715 | telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null, 716 | tagInfo: aiServiceResponse ? aiServiceResponse.tagInfo : null 717 | }; 718 | } catch (error) { 719 | // Stop any loading indicator on error 720 | if (loadingIndicator) { 721 | stopLoadingIndicator(loadingIndicator); 722 | } 723 | 724 | report(`Error adding task: ${error.message}`, 'error'); 725 | if (outputFormat === 'text') { 726 | console.error(chalk.red(`Error: ${error.message}`)); 727 | } 728 | // In MCP mode, we let the direct function handler catch and format 729 | throw error; 730 | } 731 | } 732 | 733 | export default addTask; 734 | ``` -------------------------------------------------------------------------------- /tests/integration/cli/move-cross-tag.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | 5 | // --- Define mock functions --- 6 | const mockMoveTasksBetweenTags = jest.fn(); 7 | const mockMoveTask = jest.fn(); 8 | const mockGenerateTaskFiles = jest.fn(); 9 | const mockLog = jest.fn(); 10 | 11 | // --- Setup mocks using unstable_mockModule --- 12 | jest.unstable_mockModule( 13 | '../../../scripts/modules/task-manager/move-task.js', 14 | () => ({ 15 | default: mockMoveTask, 16 | moveTasksBetweenTags: mockMoveTasksBetweenTags 17 | }) 18 | ); 19 | 20 | jest.unstable_mockModule( 21 | '../../../scripts/modules/task-manager/generate-task-files.js', 22 | () => ({ 23 | default: mockGenerateTaskFiles 24 | }) 25 | ); 26 | 27 | jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({ 28 | log: mockLog, 29 | readJSON: jest.fn(), 30 | writeJSON: jest.fn(), 31 | findProjectRoot: jest.fn(() => '/test/project/root'), 32 | getCurrentTag: jest.fn(() => 'master') 33 | })); 34 | 35 | // --- Mock chalk for consistent output formatting --- 36 | const mockChalk = { 37 | red: jest.fn((text) => text), 38 | yellow: jest.fn((text) => text), 39 | blue: jest.fn((text) => text), 40 | green: jest.fn((text) => text), 41 | gray: jest.fn((text) => text), 42 | dim: jest.fn((text) => text), 43 | bold: { 44 | cyan: jest.fn((text) => text), 45 | white: jest.fn((text) => text), 46 | red: jest.fn((text) => text) 47 | }, 48 | cyan: { 49 | bold: jest.fn((text) => text) 50 | }, 51 | white: { 52 | bold: jest.fn((text) => text) 53 | } 54 | }; 55 | 56 | jest.unstable_mockModule('chalk', () => ({ 57 | default: mockChalk 58 | })); 59 | 60 | // --- Import modules (AFTER mock setup) --- 61 | let moveTaskModule, generateTaskFilesModule, utilsModule, chalk; 62 | 63 | describe('Cross-Tag Move CLI Integration', () => { 64 | // Setup dynamic imports before tests run 65 | beforeAll(async () => { 66 | moveTaskModule = await import( 67 | '../../../scripts/modules/task-manager/move-task.js' 68 | ); 69 | generateTaskFilesModule = await import( 70 | '../../../scripts/modules/task-manager/generate-task-files.js' 71 | ); 72 | utilsModule = await import('../../../scripts/modules/utils.js'); 73 | chalk = (await import('chalk')).default; 74 | }); 75 | 76 | beforeEach(() => { 77 | jest.clearAllMocks(); 78 | }); 79 | 80 | // Helper function to capture console output and process.exit calls 81 | function captureConsoleAndExit() { 82 | const originalConsoleError = console.error; 83 | const originalConsoleLog = console.log; 84 | const originalProcessExit = process.exit; 85 | 86 | const errorMessages = []; 87 | const logMessages = []; 88 | const exitCodes = []; 89 | 90 | console.error = jest.fn((...args) => { 91 | errorMessages.push(args.join(' ')); 92 | }); 93 | 94 | console.log = jest.fn((...args) => { 95 | logMessages.push(args.join(' ')); 96 | }); 97 | 98 | process.exit = jest.fn((code) => { 99 | exitCodes.push(code); 100 | }); 101 | 102 | return { 103 | errorMessages, 104 | logMessages, 105 | exitCodes, 106 | restore: () => { 107 | console.error = originalConsoleError; 108 | console.log = originalConsoleLog; 109 | process.exit = originalProcessExit; 110 | } 111 | }; 112 | } 113 | 114 | // --- Replicate the move command action handler logic from commands.js --- 115 | async function moveAction(options) { 116 | const sourceId = options.from; 117 | const destinationId = options.to; 118 | const fromTag = options.fromTag; 119 | const toTag = options.toTag; 120 | const withDependencies = options.withDependencies; 121 | const ignoreDependencies = options.ignoreDependencies; 122 | const force = options.force; 123 | 124 | // Get the source tag - fallback to current tag if not provided 125 | const sourceTag = fromTag || utilsModule.getCurrentTag(); 126 | 127 | // Check if this is a cross-tag move (different tags) 128 | const isCrossTagMove = sourceTag && toTag && sourceTag !== toTag; 129 | 130 | if (isCrossTagMove) { 131 | // Cross-tag move logic 132 | if (!sourceId) { 133 | const error = new Error( 134 | '--from parameter is required for cross-tag moves' 135 | ); 136 | console.error(chalk.red(`Error: ${error.message}`)); 137 | throw error; 138 | } 139 | 140 | const taskIds = sourceId.split(',').map((id) => parseInt(id.trim(), 10)); 141 | 142 | // Validate parsed task IDs 143 | for (let i = 0; i < taskIds.length; i++) { 144 | if (isNaN(taskIds[i])) { 145 | const error = new Error( 146 | `Invalid task ID at position ${i + 1}: "${sourceId.split(',')[i].trim()}" is not a valid number` 147 | ); 148 | console.error(chalk.red(`Error: ${error.message}`)); 149 | throw error; 150 | } 151 | } 152 | 153 | const tasksPath = path.join( 154 | utilsModule.findProjectRoot(), 155 | '.taskmaster', 156 | 'tasks', 157 | 'tasks.json' 158 | ); 159 | 160 | try { 161 | const result = await moveTaskModule.moveTasksBetweenTags( 162 | tasksPath, 163 | taskIds, 164 | sourceTag, 165 | toTag, 166 | { 167 | withDependencies, 168 | ignoreDependencies 169 | } 170 | ); 171 | 172 | console.log(chalk.green('Successfully moved task(s) between tags')); 173 | 174 | // Print advisory tips when present 175 | if (result && Array.isArray(result.tips) && result.tips.length > 0) { 176 | console.log('Next Steps:'); 177 | result.tips.forEach((t) => console.log(` • ${t}`)); 178 | } 179 | 180 | // Generate task files for both tags 181 | await generateTaskFilesModule.default( 182 | tasksPath, 183 | path.dirname(tasksPath), 184 | { tag: sourceTag } 185 | ); 186 | await generateTaskFilesModule.default( 187 | tasksPath, 188 | path.dirname(tasksPath), 189 | { tag: toTag } 190 | ); 191 | } catch (error) { 192 | console.error(chalk.red(`Error: ${error.message}`)); 193 | // Print ID collision guidance similar to CLI help block 194 | if ( 195 | typeof error?.message === 'string' && 196 | error.message.includes('already exists in target tag') 197 | ) { 198 | console.log(''); 199 | console.log('Conflict: ID already exists in target tag'); 200 | console.log( 201 | ' • Choose a different target tag without conflicting IDs' 202 | ); 203 | console.log(' • Move a different set of IDs (avoid existing ones)'); 204 | console.log( 205 | ' • If needed, move within-tag to a new ID first, then cross-tag move' 206 | ); 207 | } 208 | throw error; 209 | } 210 | } else { 211 | // Handle case where both tags are provided but are the same 212 | if (sourceTag && toTag && sourceTag === toTag) { 213 | // If both tags are the same and we have destinationId, treat as within-tag move 214 | if (destinationId) { 215 | if (!sourceId) { 216 | const error = new Error( 217 | 'Both --from and --to parameters are required for within-tag moves' 218 | ); 219 | console.error(chalk.red(`Error: ${error.message}`)); 220 | throw error; 221 | } 222 | 223 | // Call the existing moveTask function for within-tag moves 224 | try { 225 | await moveTaskModule.default(sourceId, destinationId); 226 | console.log(chalk.green('Successfully moved task')); 227 | } catch (error) { 228 | console.error(chalk.red(`Error: ${error.message}`)); 229 | throw error; 230 | } 231 | } else { 232 | // Same tags but no destinationId - this is an error 233 | const error = new Error( 234 | `Source and target tags are the same ("${sourceTag}") but no destination specified` 235 | ); 236 | console.error(chalk.red(`Error: ${error.message}`)); 237 | console.log( 238 | chalk.yellow( 239 | 'For within-tag moves, use: task-master move --from=<sourceId> --to=<destinationId>' 240 | ) 241 | ); 242 | console.log( 243 | chalk.yellow( 244 | 'For cross-tag moves, use different tags: task-master move --from=<sourceId> --from-tag=<sourceTag> --to-tag=<targetTag>' 245 | ) 246 | ); 247 | throw error; 248 | } 249 | } else { 250 | // Within-tag move logic (existing functionality) 251 | if (!sourceId || !destinationId) { 252 | const error = new Error( 253 | 'Both --from and --to parameters are required for within-tag moves' 254 | ); 255 | console.error(chalk.red(`Error: ${error.message}`)); 256 | throw error; 257 | } 258 | 259 | // Call the existing moveTask function for within-tag moves 260 | try { 261 | await moveTaskModule.default(sourceId, destinationId); 262 | console.log(chalk.green('Successfully moved task')); 263 | } catch (error) { 264 | console.error(chalk.red(`Error: ${error.message}`)); 265 | throw error; 266 | } 267 | } 268 | } 269 | } 270 | 271 | it('should move task without dependencies successfully', async () => { 272 | // Mock successful cross-tag move 273 | mockMoveTasksBetweenTags.mockResolvedValue(undefined); 274 | mockGenerateTaskFiles.mockResolvedValue(undefined); 275 | 276 | const options = { 277 | from: '2', 278 | fromTag: 'backlog', 279 | toTag: 'in-progress' 280 | }; 281 | 282 | await moveAction(options); 283 | 284 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith( 285 | expect.stringContaining('tasks.json'), 286 | [2], 287 | 'backlog', 288 | 'in-progress', 289 | { 290 | withDependencies: undefined, 291 | ignoreDependencies: undefined 292 | } 293 | ); 294 | }); 295 | 296 | it('should fail to move task with cross-tag dependencies', async () => { 297 | // Mock dependency conflict error 298 | mockMoveTasksBetweenTags.mockRejectedValue( 299 | new Error('Cannot move task due to cross-tag dependency conflicts') 300 | ); 301 | 302 | const options = { 303 | from: '1', 304 | fromTag: 'backlog', 305 | toTag: 'in-progress' 306 | }; 307 | 308 | const { errorMessages, restore } = captureConsoleAndExit(); 309 | 310 | await expect(moveAction(options)).rejects.toThrow( 311 | 'Cannot move task due to cross-tag dependency conflicts' 312 | ); 313 | 314 | expect(mockMoveTasksBetweenTags).toHaveBeenCalled(); 315 | expect( 316 | errorMessages.some((msg) => 317 | msg.includes('cross-tag dependency conflicts') 318 | ) 319 | ).toBe(true); 320 | 321 | restore(); 322 | }); 323 | 324 | it('should move task with dependencies when --with-dependencies is used', async () => { 325 | // Mock successful cross-tag move with dependencies 326 | mockMoveTasksBetweenTags.mockResolvedValue(undefined); 327 | mockGenerateTaskFiles.mockResolvedValue(undefined); 328 | 329 | const options = { 330 | from: '1', 331 | fromTag: 'backlog', 332 | toTag: 'in-progress', 333 | withDependencies: true 334 | }; 335 | 336 | await moveAction(options); 337 | 338 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith( 339 | expect.stringContaining('tasks.json'), 340 | [1], 341 | 'backlog', 342 | 'in-progress', 343 | { 344 | withDependencies: true, 345 | ignoreDependencies: undefined 346 | } 347 | ); 348 | }); 349 | 350 | it('should break dependencies when --ignore-dependencies is used', async () => { 351 | // Mock successful cross-tag move with dependency breaking 352 | mockMoveTasksBetweenTags.mockResolvedValue(undefined); 353 | mockGenerateTaskFiles.mockResolvedValue(undefined); 354 | 355 | const options = { 356 | from: '1', 357 | fromTag: 'backlog', 358 | toTag: 'in-progress', 359 | ignoreDependencies: true 360 | }; 361 | 362 | await moveAction(options); 363 | 364 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith( 365 | expect.stringContaining('tasks.json'), 366 | [1], 367 | 'backlog', 368 | 'in-progress', 369 | { 370 | withDependencies: undefined, 371 | ignoreDependencies: true 372 | } 373 | ); 374 | }); 375 | 376 | it('should create target tag if it does not exist', async () => { 377 | // Mock successful cross-tag move to new tag 378 | mockMoveTasksBetweenTags.mockResolvedValue(undefined); 379 | mockGenerateTaskFiles.mockResolvedValue(undefined); 380 | 381 | const options = { 382 | from: '2', 383 | fromTag: 'backlog', 384 | toTag: 'new-tag' 385 | }; 386 | 387 | await moveAction(options); 388 | 389 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith( 390 | expect.stringContaining('tasks.json'), 391 | [2], 392 | 'backlog', 393 | 'new-tag', 394 | { 395 | withDependencies: undefined, 396 | ignoreDependencies: undefined 397 | } 398 | ); 399 | }); 400 | 401 | it('should fail to move a subtask directly', async () => { 402 | // Mock subtask movement error 403 | mockMoveTasksBetweenTags.mockRejectedValue( 404 | new Error( 405 | 'Cannot move subtasks directly between tags. Please promote the subtask to a full task first.' 406 | ) 407 | ); 408 | 409 | const options = { 410 | from: '1.2', 411 | fromTag: 'backlog', 412 | toTag: 'in-progress' 413 | }; 414 | 415 | const { errorMessages, restore } = captureConsoleAndExit(); 416 | 417 | await expect(moveAction(options)).rejects.toThrow( 418 | 'Cannot move subtasks directly between tags. Please promote the subtask to a full task first.' 419 | ); 420 | 421 | expect(mockMoveTasksBetweenTags).toHaveBeenCalled(); 422 | expect(errorMessages.some((msg) => msg.includes('subtasks directly'))).toBe( 423 | true 424 | ); 425 | 426 | restore(); 427 | }); 428 | 429 | it('should provide helpful error messages for dependency conflicts', async () => { 430 | // Mock dependency conflict with detailed error 431 | mockMoveTasksBetweenTags.mockRejectedValue( 432 | new Error( 433 | 'Cross-tag dependency conflicts detected. Task 1 depends on Task 2 which is in a different tag.' 434 | ) 435 | ); 436 | 437 | const options = { 438 | from: '1', 439 | fromTag: 'backlog', 440 | toTag: 'in-progress' 441 | }; 442 | 443 | const { errorMessages, restore } = captureConsoleAndExit(); 444 | 445 | await expect(moveAction(options)).rejects.toThrow( 446 | 'Cross-tag dependency conflicts detected. Task 1 depends on Task 2 which is in a different tag.' 447 | ); 448 | 449 | expect(mockMoveTasksBetweenTags).toHaveBeenCalled(); 450 | expect( 451 | errorMessages.some((msg) => 452 | msg.includes('Cross-tag dependency conflicts detected') 453 | ) 454 | ).toBe(true); 455 | 456 | restore(); 457 | }); 458 | 459 | it('should print advisory tips when result.tips are returned (ignore-dependencies)', async () => { 460 | const { errorMessages, logMessages, restore } = captureConsoleAndExit(); 461 | try { 462 | // Arrange: mock move to return tips 463 | mockMoveTasksBetweenTags.mockResolvedValue({ 464 | message: 'ok', 465 | tips: [ 466 | 'Run "task-master validate-dependencies" to check for dependency issues.', 467 | 'Run "task-master fix-dependencies" to automatically repair dangling dependencies.' 468 | ] 469 | }); 470 | 471 | await moveAction({ 472 | from: '2', 473 | fromTag: 'backlog', 474 | toTag: 'in-progress', 475 | ignoreDependencies: true 476 | }); 477 | 478 | const joined = logMessages.join('\n'); 479 | expect(joined).toContain('Next Steps'); 480 | expect(joined).toContain('validate-dependencies'); 481 | expect(joined).toContain('fix-dependencies'); 482 | } finally { 483 | restore(); 484 | } 485 | }); 486 | 487 | it('should print ID collision suggestions when target already has the ID', async () => { 488 | const { errorMessages, logMessages, restore } = captureConsoleAndExit(); 489 | try { 490 | // Arrange: mock move to throw collision 491 | const err = new Error( 492 | 'Task 1 already exists in target tag "in-progress"' 493 | ); 494 | mockMoveTasksBetweenTags.mockRejectedValue(err); 495 | 496 | await expect( 497 | moveAction({ from: '1', fromTag: 'backlog', toTag: 'in-progress' }) 498 | ).rejects.toThrow('already exists in target tag'); 499 | 500 | const joined = logMessages.join('\n'); 501 | expect(joined).toContain('Conflict: ID already exists in target tag'); 502 | expect(joined).toContain('different target tag'); 503 | expect(joined).toContain('different set of IDs'); 504 | expect(joined).toContain('within-tag'); 505 | } finally { 506 | restore(); 507 | } 508 | }); 509 | 510 | it('should handle same tag error correctly', async () => { 511 | const options = { 512 | from: '1', 513 | fromTag: 'backlog', 514 | toTag: 'backlog' // Same tag but no destination 515 | }; 516 | 517 | const { errorMessages, logMessages, restore } = captureConsoleAndExit(); 518 | 519 | await expect(moveAction(options)).rejects.toThrow( 520 | 'Source and target tags are the same ("backlog") but no destination specified' 521 | ); 522 | 523 | expect( 524 | errorMessages.some((msg) => 525 | msg.includes( 526 | 'Source and target tags are the same ("backlog") but no destination specified' 527 | ) 528 | ) 529 | ).toBe(true); 530 | expect( 531 | logMessages.some((msg) => msg.includes('For within-tag moves')) 532 | ).toBe(true); 533 | expect(logMessages.some((msg) => msg.includes('For cross-tag moves'))).toBe( 534 | true 535 | ); 536 | 537 | restore(); 538 | }); 539 | 540 | it('should use current tag when --from-tag is not provided', async () => { 541 | // Mock successful move with current tag fallback 542 | mockMoveTasksBetweenTags.mockResolvedValue({ 543 | message: 'Successfully moved task(s) between tags' 544 | }); 545 | 546 | // Mock getCurrentTag to return 'master' 547 | utilsModule.getCurrentTag.mockReturnValue('master'); 548 | 549 | // Simulate command: task-master move --from=1 --to-tag=in-progress 550 | // (no --from-tag provided, should use current tag 'master') 551 | await moveAction({ 552 | from: '1', 553 | toTag: 'in-progress', 554 | withDependencies: false, 555 | ignoreDependencies: false 556 | // fromTag is intentionally not provided to test fallback 557 | }); 558 | 559 | // Verify that moveTasksBetweenTags was called with 'master' as source tag 560 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith( 561 | expect.stringContaining('.taskmaster/tasks/tasks.json'), 562 | [1], // parseInt converts string to number 563 | 'master', // Should use current tag as fallback 564 | 'in-progress', 565 | { 566 | withDependencies: false, 567 | ignoreDependencies: false 568 | } 569 | ); 570 | 571 | // Verify that generateTaskFiles was called for both tags 572 | expect(generateTaskFilesModule.default).toHaveBeenCalledWith( 573 | expect.stringContaining('.taskmaster/tasks/tasks.json'), 574 | expect.stringContaining('.taskmaster/tasks'), 575 | { tag: 'master' } 576 | ); 577 | expect(generateTaskFilesModule.default).toHaveBeenCalledWith( 578 | expect.stringContaining('.taskmaster/tasks/tasks.json'), 579 | expect.stringContaining('.taskmaster/tasks'), 580 | { tag: 'in-progress' } 581 | ); 582 | }); 583 | 584 | it('should move multiple tasks with comma-separated IDs successfully', async () => { 585 | // Mock successful cross-tag move for multiple tasks 586 | mockMoveTasksBetweenTags.mockResolvedValue(undefined); 587 | mockGenerateTaskFiles.mockResolvedValue(undefined); 588 | 589 | const options = { 590 | from: '1,2,3', 591 | fromTag: 'backlog', 592 | toTag: 'in-progress' 593 | }; 594 | 595 | await moveAction(options); 596 | 597 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith( 598 | expect.stringContaining('tasks.json'), 599 | [1, 2, 3], // Should parse comma-separated string to array of integers 600 | 'backlog', 601 | 'in-progress', 602 | { 603 | withDependencies: undefined, 604 | ignoreDependencies: undefined 605 | } 606 | ); 607 | 608 | // Verify task files are generated for both tags 609 | expect(mockGenerateTaskFiles).toHaveBeenCalledTimes(2); 610 | expect(mockGenerateTaskFiles).toHaveBeenCalledWith( 611 | expect.stringContaining('tasks.json'), 612 | expect.stringContaining('.taskmaster/tasks'), 613 | { tag: 'backlog' } 614 | ); 615 | expect(mockGenerateTaskFiles).toHaveBeenCalledWith( 616 | expect.stringContaining('tasks.json'), 617 | expect.stringContaining('.taskmaster/tasks'), 618 | { tag: 'in-progress' } 619 | ); 620 | }); 621 | 622 | // Note: --force flag is no longer supported for cross-tag moves 623 | 624 | it('should fail when invalid task ID is provided', async () => { 625 | const options = { 626 | from: '1,abc,3', // Invalid ID in middle 627 | fromTag: 'backlog', 628 | toTag: 'in-progress' 629 | }; 630 | 631 | const { errorMessages, restore } = captureConsoleAndExit(); 632 | 633 | await expect(moveAction(options)).rejects.toThrow( 634 | 'Invalid task ID at position 2: "abc" is not a valid number' 635 | ); 636 | 637 | expect( 638 | errorMessages.some((msg) => msg.includes('Invalid task ID at position 2')) 639 | ).toBe(true); 640 | 641 | restore(); 642 | }); 643 | 644 | it('should fail when first task ID is invalid', async () => { 645 | const options = { 646 | from: 'abc,2,3', // Invalid ID at start 647 | fromTag: 'backlog', 648 | toTag: 'in-progress' 649 | }; 650 | 651 | const { errorMessages, restore } = captureConsoleAndExit(); 652 | 653 | await expect(moveAction(options)).rejects.toThrow( 654 | 'Invalid task ID at position 1: "abc" is not a valid number' 655 | ); 656 | 657 | expect( 658 | errorMessages.some((msg) => msg.includes('Invalid task ID at position 1')) 659 | ).toBe(true); 660 | 661 | restore(); 662 | }); 663 | 664 | it('should fail when last task ID is invalid', async () => { 665 | const options = { 666 | from: '1,2,xyz', // Invalid ID at end 667 | fromTag: 'backlog', 668 | toTag: 'in-progress' 669 | }; 670 | 671 | const { errorMessages, restore } = captureConsoleAndExit(); 672 | 673 | await expect(moveAction(options)).rejects.toThrow( 674 | 'Invalid task ID at position 3: "xyz" is not a valid number' 675 | ); 676 | 677 | expect( 678 | errorMessages.some((msg) => msg.includes('Invalid task ID at position 3')) 679 | ).toBe(true); 680 | 681 | restore(); 682 | }); 683 | 684 | it('should fail when single invalid task ID is provided', async () => { 685 | const options = { 686 | from: 'invalid', 687 | fromTag: 'backlog', 688 | toTag: 'in-progress' 689 | }; 690 | 691 | const { errorMessages, restore } = captureConsoleAndExit(); 692 | 693 | await expect(moveAction(options)).rejects.toThrow( 694 | 'Invalid task ID at position 1: "invalid" is not a valid number' 695 | ); 696 | 697 | expect( 698 | errorMessages.some((msg) => msg.includes('Invalid task ID at position 1')) 699 | ).toBe(true); 700 | 701 | restore(); 702 | }); 703 | 704 | // Note: --force combinations removed 705 | 706 | // Note: --force combinations removed 707 | 708 | // Note: --force combinations removed 709 | 710 | it('should handle whitespace in comma-separated task IDs', async () => { 711 | // Mock successful cross-tag move with whitespace 712 | mockMoveTasksBetweenTags.mockResolvedValue(undefined); 713 | mockGenerateTaskFiles.mockResolvedValue(undefined); 714 | 715 | const options = { 716 | from: ' 1 , 2 , 3 ', // Whitespace around IDs and commas 717 | fromTag: 'backlog', 718 | toTag: 'in-progress' 719 | }; 720 | 721 | await moveAction(options); 722 | 723 | expect(mockMoveTasksBetweenTags).toHaveBeenCalledWith( 724 | expect.stringContaining('tasks.json'), 725 | [1, 2, 3], // Should trim whitespace and parse as integers 726 | 'backlog', 727 | 'in-progress', 728 | { 729 | withDependencies: undefined, 730 | ignoreDependencies: undefined, 731 | force: undefined 732 | } 733 | ); 734 | }); 735 | 736 | it('should fail when --from parameter is missing for cross-tag move', async () => { 737 | const options = { 738 | fromTag: 'backlog', 739 | toTag: 'in-progress' 740 | // from is intentionally missing 741 | }; 742 | 743 | const { errorMessages, restore } = captureConsoleAndExit(); 744 | 745 | await expect(moveAction(options)).rejects.toThrow( 746 | '--from parameter is required for cross-tag moves' 747 | ); 748 | 749 | expect( 750 | errorMessages.some((msg) => 751 | msg.includes('--from parameter is required for cross-tag moves') 752 | ) 753 | ).toBe(true); 754 | 755 | restore(); 756 | }); 757 | 758 | it('should fail when both --from and --to are missing for within-tag move', async () => { 759 | const options = { 760 | // Both from and to are missing for within-tag move 761 | }; 762 | 763 | const { errorMessages, restore } = captureConsoleAndExit(); 764 | 765 | await expect(moveAction(options)).rejects.toThrow( 766 | 'Both --from and --to parameters are required for within-tag moves' 767 | ); 768 | 769 | expect( 770 | errorMessages.some((msg) => 771 | msg.includes( 772 | 'Both --from and --to parameters are required for within-tag moves' 773 | ) 774 | ) 775 | ).toBe(true); 776 | 777 | restore(); 778 | }); 779 | 780 | it('should handle within-tag move when only --from is provided', async () => { 781 | // Mock successful within-tag move 782 | mockMoveTask.mockResolvedValue(undefined); 783 | 784 | const options = { 785 | from: '1', 786 | to: '2' 787 | // No tags specified, should use within-tag logic 788 | }; 789 | 790 | await moveAction(options); 791 | 792 | expect(mockMoveTask).toHaveBeenCalledWith('1', '2'); 793 | expect(mockMoveTasksBetweenTags).not.toHaveBeenCalled(); 794 | }); 795 | 796 | it('should handle within-tag move when both tags are the same', async () => { 797 | // Mock successful within-tag move 798 | mockMoveTask.mockResolvedValue(undefined); 799 | 800 | const options = { 801 | from: '1', 802 | to: '2', 803 | fromTag: 'master', 804 | toTag: 'master' // Same tag, should use within-tag logic 805 | }; 806 | 807 | await moveAction(options); 808 | 809 | expect(mockMoveTask).toHaveBeenCalledWith('1', '2'); 810 | expect(mockMoveTasksBetweenTags).not.toHaveBeenCalled(); 811 | }); 812 | 813 | it('should fail when both tags are the same but no destination is provided', async () => { 814 | const options = { 815 | from: '1', 816 | fromTag: 'master', 817 | toTag: 'master' // Same tag but no destination 818 | }; 819 | 820 | const { errorMessages, logMessages, restore } = captureConsoleAndExit(); 821 | 822 | await expect(moveAction(options)).rejects.toThrow( 823 | 'Source and target tags are the same ("master") but no destination specified' 824 | ); 825 | 826 | expect( 827 | errorMessages.some((msg) => 828 | msg.includes( 829 | 'Source and target tags are the same ("master") but no destination specified' 830 | ) 831 | ) 832 | ).toBe(true); 833 | expect( 834 | logMessages.some((msg) => msg.includes('For within-tag moves')) 835 | ).toBe(true); 836 | expect(logMessages.some((msg) => msg.includes('For cross-tag moves'))).toBe( 837 | true 838 | ); 839 | 840 | restore(); 841 | }); 842 | }); 843 | ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/analyze-task-complexity.js: -------------------------------------------------------------------------------- ```javascript 1 | import chalk from 'chalk'; 2 | import boxen from 'boxen'; 3 | import readline from 'readline'; 4 | import fs from 'fs'; 5 | 6 | import { log, readJSON, isSilentMode } from '../utils.js'; 7 | 8 | import { 9 | startLoadingIndicator, 10 | stopLoadingIndicator, 11 | displayAiUsageSummary 12 | } from '../ui.js'; 13 | 14 | import { generateTextService } from '../ai-services-unified.js'; 15 | 16 | import { 17 | getDebugFlag, 18 | getProjectName, 19 | hasCodebaseAnalysis 20 | } from '../config-manager.js'; 21 | import { getPromptManager } from '../prompt-manager.js'; 22 | import { 23 | COMPLEXITY_REPORT_FILE, 24 | LEGACY_TASKS_FILE 25 | } from '../../../src/constants/paths.js'; 26 | import { CUSTOM_PROVIDERS } from '../../../src/constants/providers.js'; 27 | import { resolveComplexityReportOutputPath } from '../../../src/utils/path-utils.js'; 28 | import { ContextGatherer } from '../utils/contextGatherer.js'; 29 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js'; 30 | import { flattenTasksWithSubtasks } from '../utils.js'; 31 | 32 | /** 33 | * Generates the prompt for complexity analysis. 34 | * (Moved from ai-services.js and simplified) 35 | * @param {Object} tasksData - The tasks data object. 36 | * @param {string} [gatheredContext] - The gathered context for the analysis. 37 | * @returns {string} The generated prompt. 38 | */ 39 | function generateInternalComplexityAnalysisPrompt( 40 | tasksData, 41 | gatheredContext = '' 42 | ) { 43 | const tasksString = JSON.stringify(tasksData.tasks, null, 2); 44 | let prompt = `Analyze the following tasks to determine their complexity (1-10 scale) and recommend the number of subtasks for expansion. Provide a brief reasoning and an initial expansion prompt for each. 45 | 46 | Tasks: 47 | ${tasksString}`; 48 | 49 | if (gatheredContext) { 50 | prompt += `\n\n# Project Context\n\n${gatheredContext}`; 51 | } 52 | 53 | prompt += ` 54 | 55 | Respond ONLY with a valid JSON array matching the schema: 56 | [ 57 | { 58 | "taskId": <number>, 59 | "taskTitle": "<string>", 60 | "complexityScore": <number 1-10>, 61 | "recommendedSubtasks": <number>, 62 | "expansionPrompt": "<string>", 63 | "reasoning": "<string>" 64 | }, 65 | ... 66 | ] 67 | 68 | Do not include any explanatory text, markdown formatting, or code block markers before or after the JSON array.`; 69 | return prompt; 70 | } 71 | 72 | /** 73 | * Analyzes task complexity and generates expansion recommendations 74 | * @param {Object} options Command options 75 | * @param {string} options.file - Path to tasks file 76 | * @param {string} options.output - Path to report output file 77 | * @param {string|number} [options.threshold] - Complexity threshold 78 | * @param {boolean} [options.research] - Use research role 79 | * @param {string} [options.projectRoot] - Project root path (for MCP/env fallback). 80 | * @param {string} [options.tag] - Tag for the task 81 | * @param {string} [options.id] - Comma-separated list of task IDs to analyze specifically 82 | * @param {number} [options.from] - Starting task ID in a range to analyze 83 | * @param {number} [options.to] - Ending task ID in a range to analyze 84 | * @param {Object} [options._filteredTasksData] - Pre-filtered task data (internal use) 85 | * @param {number} [options._originalTaskCount] - Original task count (internal use) 86 | * @param {Object} context - Context object, potentially containing session and mcpLog 87 | * @param {Object} [context.session] - Session object from MCP server (optional) 88 | * @param {Object} [context.mcpLog] - MCP logger object (optional) 89 | * @param {function} [context.reportProgress] - Deprecated: Function to report progress (ignored) 90 | */ 91 | async function analyzeTaskComplexity(options, context = {}) { 92 | const { session, mcpLog } = context; 93 | const tasksPath = options.file || LEGACY_TASKS_FILE; 94 | const thresholdScore = parseFloat(options.threshold || '5'); 95 | const useResearch = options.research || false; 96 | const projectRoot = options.projectRoot; 97 | const tag = options.tag; 98 | // New parameters for task ID filtering 99 | const specificIds = options.id 100 | ? options.id 101 | .split(',') 102 | .map((id) => parseInt(id.trim(), 10)) 103 | .filter((id) => !Number.isNaN(id)) 104 | : null; 105 | const fromId = options.from !== undefined ? parseInt(options.from, 10) : null; 106 | const toId = options.to !== undefined ? parseInt(options.to, 10) : null; 107 | 108 | const outputFormat = mcpLog ? 'json' : 'text'; 109 | 110 | const reportLog = (message, level = 'info') => { 111 | if (mcpLog) { 112 | mcpLog[level](message); 113 | } else if (!isSilentMode() && outputFormat === 'text') { 114 | log(level, message); 115 | } 116 | }; 117 | 118 | // Resolve output path using tag-aware resolution 119 | const outputPath = resolveComplexityReportOutputPath( 120 | options.output, 121 | { projectRoot, tag }, 122 | reportLog 123 | ); 124 | 125 | if (outputFormat === 'text') { 126 | console.log( 127 | chalk.blue( 128 | 'Analyzing task complexity and generating expansion recommendations...' 129 | ) 130 | ); 131 | } 132 | 133 | try { 134 | reportLog(`Reading tasks from ${tasksPath}...`, 'info'); 135 | let tasksData; 136 | let originalTaskCount = 0; 137 | let originalData = null; 138 | 139 | if (options._filteredTasksData) { 140 | tasksData = options._filteredTasksData; 141 | originalTaskCount = options._originalTaskCount || tasksData.tasks.length; 142 | if (!options._originalTaskCount) { 143 | try { 144 | originalData = readJSON(tasksPath, projectRoot, tag); 145 | if (originalData && originalData.tasks) { 146 | originalTaskCount = originalData.tasks.length; 147 | } 148 | } catch (e) { 149 | log('warn', `Could not read original tasks file: ${e.message}`); 150 | } 151 | } 152 | } else { 153 | originalData = readJSON(tasksPath, projectRoot, tag); 154 | if ( 155 | !originalData || 156 | !originalData.tasks || 157 | !Array.isArray(originalData.tasks) || 158 | originalData.tasks.length === 0 159 | ) { 160 | throw new Error('No tasks found in the tasks file'); 161 | } 162 | originalTaskCount = originalData.tasks.length; 163 | 164 | // Filter tasks based on active status 165 | const activeStatuses = ['pending', 'blocked', 'in-progress']; 166 | let filteredTasks = originalData.tasks.filter((task) => 167 | activeStatuses.includes(task.status?.toLowerCase() || 'pending') 168 | ); 169 | 170 | // Apply ID filtering if specified 171 | if (specificIds && specificIds.length > 0) { 172 | reportLog( 173 | `Filtering tasks by specific IDs: ${specificIds.join(', ')}`, 174 | 'info' 175 | ); 176 | filteredTasks = filteredTasks.filter((task) => 177 | specificIds.includes(task.id) 178 | ); 179 | 180 | if (outputFormat === 'text') { 181 | if (filteredTasks.length === 0 && specificIds.length > 0) { 182 | console.log( 183 | chalk.yellow( 184 | `Warning: No active tasks found with IDs: ${specificIds.join(', ')}` 185 | ) 186 | ); 187 | } else if (filteredTasks.length < specificIds.length) { 188 | const foundIds = filteredTasks.map((t) => t.id); 189 | const missingIds = specificIds.filter( 190 | (id) => !foundIds.includes(id) 191 | ); 192 | console.log( 193 | chalk.yellow( 194 | `Warning: Some requested task IDs were not found or are not active: ${missingIds.join(', ')}` 195 | ) 196 | ); 197 | } 198 | } 199 | } 200 | // Apply range filtering if specified 201 | else if (fromId !== null || toId !== null) { 202 | const effectiveFromId = fromId !== null ? fromId : 1; 203 | const effectiveToId = 204 | toId !== null 205 | ? toId 206 | : Math.max(...originalData.tasks.map((t) => t.id)); 207 | 208 | reportLog( 209 | `Filtering tasks by ID range: ${effectiveFromId} to ${effectiveToId}`, 210 | 'info' 211 | ); 212 | filteredTasks = filteredTasks.filter( 213 | (task) => task.id >= effectiveFromId && task.id <= effectiveToId 214 | ); 215 | 216 | if (outputFormat === 'text' && filteredTasks.length === 0) { 217 | console.log( 218 | chalk.yellow( 219 | `Warning: No active tasks found in range: ${effectiveFromId}-${effectiveToId}` 220 | ) 221 | ); 222 | } 223 | } 224 | 225 | tasksData = { 226 | ...originalData, 227 | tasks: filteredTasks, 228 | _originalTaskCount: originalTaskCount 229 | }; 230 | } 231 | 232 | // --- Context Gathering --- 233 | let gatheredContext = ''; 234 | if (originalData && originalData.tasks.length > 0) { 235 | try { 236 | const contextGatherer = new ContextGatherer(projectRoot, tag); 237 | const allTasksFlat = flattenTasksWithSubtasks(originalData.tasks); 238 | const fuzzySearch = new FuzzyTaskSearch( 239 | allTasksFlat, 240 | 'analyze-complexity' 241 | ); 242 | // Create a query from the tasks being analyzed 243 | const searchQuery = tasksData.tasks 244 | .map((t) => `${t.title} ${t.description}`) 245 | .join(' '); 246 | const searchResults = fuzzySearch.findRelevantTasks(searchQuery, { 247 | maxResults: 10 248 | }); 249 | const relevantTaskIds = fuzzySearch.getTaskIds(searchResults); 250 | 251 | if (relevantTaskIds.length > 0) { 252 | const contextResult = await contextGatherer.gather({ 253 | tasks: relevantTaskIds, 254 | format: 'research' 255 | }); 256 | gatheredContext = contextResult.context || ''; 257 | } 258 | } catch (contextError) { 259 | reportLog( 260 | `Could not gather additional context: ${contextError.message}`, 261 | 'warn' 262 | ); 263 | } 264 | } 265 | // --- End Context Gathering --- 266 | 267 | const skippedCount = originalTaskCount - tasksData.tasks.length; 268 | reportLog( 269 | `Found ${originalTaskCount} total tasks in the task file.`, 270 | 'info' 271 | ); 272 | 273 | // Updated messaging to reflect filtering logic 274 | if (specificIds || fromId !== null || toId !== null) { 275 | const filterMsg = specificIds 276 | ? `Analyzing ${tasksData.tasks.length} tasks with specific IDs: ${specificIds.join(', ')}` 277 | : `Analyzing ${tasksData.tasks.length} tasks in range: ${fromId || 1} to ${toId || 'end'}`; 278 | 279 | reportLog(filterMsg, 'info'); 280 | if (outputFormat === 'text') { 281 | console.log(chalk.blue(filterMsg)); 282 | } 283 | } else if (skippedCount > 0) { 284 | const skipMessage = `Skipping ${skippedCount} tasks marked as done/cancelled/deferred. Analyzing ${tasksData.tasks.length} active tasks.`; 285 | reportLog(skipMessage, 'info'); 286 | if (outputFormat === 'text') { 287 | console.log(chalk.yellow(skipMessage)); 288 | } 289 | } 290 | 291 | // Check for existing report before doing analysis 292 | let existingReport = null; 293 | const existingAnalysisMap = new Map(); // For quick lookups by task ID 294 | try { 295 | if (fs.existsSync(outputPath)) { 296 | existingReport = JSON.parse(fs.readFileSync(outputPath, 'utf8')); 297 | reportLog(`Found existing complexity report at ${outputPath}`, 'info'); 298 | 299 | if ( 300 | existingReport && 301 | existingReport.complexityAnalysis && 302 | Array.isArray(existingReport.complexityAnalysis) 303 | ) { 304 | // Create lookup map of existing analysis entries 305 | existingReport.complexityAnalysis.forEach((item) => { 306 | existingAnalysisMap.set(item.taskId, item); 307 | }); 308 | reportLog( 309 | `Existing report contains ${existingReport.complexityAnalysis.length} task analyses`, 310 | 'info' 311 | ); 312 | } 313 | } 314 | } catch (readError) { 315 | reportLog( 316 | `Warning: Could not read existing report: ${readError.message}`, 317 | 'warn' 318 | ); 319 | existingReport = null; 320 | existingAnalysisMap.clear(); 321 | } 322 | 323 | if (tasksData.tasks.length === 0) { 324 | // If using ID filtering but no matching tasks, return existing report or empty 325 | if (existingReport && (specificIds || fromId !== null || toId !== null)) { 326 | reportLog( 327 | 'No matching tasks found for analysis. Keeping existing report.', 328 | 'info' 329 | ); 330 | if (outputFormat === 'text') { 331 | console.log( 332 | chalk.yellow( 333 | 'No matching tasks found for analysis. Keeping existing report.' 334 | ) 335 | ); 336 | } 337 | return { 338 | report: existingReport, 339 | telemetryData: null 340 | }; 341 | } 342 | 343 | // Otherwise create empty report 344 | const emptyReport = { 345 | meta: { 346 | generatedAt: new Date().toISOString(), 347 | tasksAnalyzed: 0, 348 | thresholdScore: thresholdScore, 349 | projectName: getProjectName(session), 350 | usedResearch: useResearch 351 | }, 352 | complexityAnalysis: existingReport?.complexityAnalysis || [] 353 | }; 354 | reportLog(`Writing complexity report to ${outputPath}...`, 'info'); 355 | fs.writeFileSync( 356 | outputPath, 357 | JSON.stringify(emptyReport, null, '\t'), 358 | 'utf8' 359 | ); 360 | reportLog( 361 | `Task complexity analysis complete. Report written to ${outputPath}`, 362 | 'success' 363 | ); 364 | if (outputFormat === 'text') { 365 | console.log( 366 | chalk.green( 367 | `Task complexity analysis complete. Report written to ${outputPath}` 368 | ) 369 | ); 370 | const highComplexity = 0; 371 | const mediumComplexity = 0; 372 | const lowComplexity = 0; 373 | const totalAnalyzed = 0; 374 | 375 | console.log('\nComplexity Analysis Summary:'); 376 | console.log('----------------------------'); 377 | console.log(`Tasks in input file: ${originalTaskCount}`); 378 | console.log(`Tasks successfully analyzed: ${totalAnalyzed}`); 379 | console.log(`High complexity tasks: ${highComplexity}`); 380 | console.log(`Medium complexity tasks: ${mediumComplexity}`); 381 | console.log(`Low complexity tasks: ${lowComplexity}`); 382 | console.log( 383 | `Sum verification: ${highComplexity + mediumComplexity + lowComplexity} (should equal ${totalAnalyzed})` 384 | ); 385 | console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`); 386 | console.log( 387 | `\nSee ${outputPath} for the full report and expansion commands.` 388 | ); 389 | 390 | console.log( 391 | boxen( 392 | chalk.white.bold('Suggested Next Steps:') + 393 | '\n\n' + 394 | `${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` + 395 | `${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` + 396 | `${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`, 397 | { 398 | padding: 1, 399 | borderColor: 'cyan', 400 | borderStyle: 'round', 401 | margin: { top: 1 } 402 | } 403 | ) 404 | ); 405 | } 406 | return { 407 | report: emptyReport, 408 | telemetryData: null 409 | }; 410 | } 411 | 412 | // Continue with regular analysis path 413 | // Load prompts using PromptManager 414 | const promptManager = getPromptManager(); 415 | 416 | // Check if Claude Code is being used as the provider 417 | 418 | const promptParams = { 419 | tasks: tasksData.tasks, 420 | gatheredContext: gatheredContext || '', 421 | useResearch: useResearch, 422 | hasCodebaseAnalysis: hasCodebaseAnalysis( 423 | useResearch, 424 | projectRoot, 425 | session 426 | ), 427 | projectRoot: projectRoot || '' 428 | }; 429 | 430 | const { systemPrompt, userPrompt: prompt } = await promptManager.loadPrompt( 431 | 'analyze-complexity', 432 | promptParams, 433 | 'default' 434 | ); 435 | 436 | let loadingIndicator = null; 437 | if (outputFormat === 'text') { 438 | loadingIndicator = startLoadingIndicator( 439 | `${useResearch ? 'Researching' : 'Analyzing'} the complexity of your tasks with AI...\n` 440 | ); 441 | } 442 | 443 | let aiServiceResponse = null; 444 | let complexityAnalysis = null; 445 | 446 | try { 447 | const role = useResearch ? 'research' : 'main'; 448 | 449 | aiServiceResponse = await generateTextService({ 450 | prompt, 451 | systemPrompt, 452 | role, 453 | session, 454 | projectRoot, 455 | commandName: 'analyze-complexity', 456 | outputType: mcpLog ? 'mcp' : 'cli' 457 | }); 458 | 459 | if (loadingIndicator) { 460 | stopLoadingIndicator(loadingIndicator); 461 | loadingIndicator = null; 462 | } 463 | if (outputFormat === 'text') { 464 | readline.clearLine(process.stdout, 0); 465 | readline.cursorTo(process.stdout, 0); 466 | console.log( 467 | chalk.green('AI service call complete. Parsing response...') 468 | ); 469 | } 470 | 471 | reportLog('Parsing complexity analysis from text response...', 'info'); 472 | try { 473 | let cleanedResponse = aiServiceResponse.mainResult; 474 | cleanedResponse = cleanedResponse.trim(); 475 | 476 | const codeBlockMatch = cleanedResponse.match( 477 | /```(?:json)?\s*([\s\S]*?)\s*```/ 478 | ); 479 | if (codeBlockMatch) { 480 | cleanedResponse = codeBlockMatch[1].trim(); 481 | } else { 482 | const firstBracket = cleanedResponse.indexOf('['); 483 | const lastBracket = cleanedResponse.lastIndexOf(']'); 484 | if (firstBracket !== -1 && lastBracket > firstBracket) { 485 | cleanedResponse = cleanedResponse.substring( 486 | firstBracket, 487 | lastBracket + 1 488 | ); 489 | } else { 490 | reportLog( 491 | 'Warning: Response does not appear to be a JSON array.', 492 | 'warn' 493 | ); 494 | } 495 | } 496 | 497 | if (outputFormat === 'text' && getDebugFlag(session)) { 498 | console.log(chalk.gray('Attempting to parse cleaned JSON...')); 499 | console.log(chalk.gray('Cleaned response (first 100 chars):')); 500 | console.log(chalk.gray(cleanedResponse.substring(0, 100))); 501 | console.log(chalk.gray('Last 100 chars:')); 502 | console.log( 503 | chalk.gray(cleanedResponse.substring(cleanedResponse.length - 100)) 504 | ); 505 | } 506 | 507 | complexityAnalysis = JSON.parse(cleanedResponse); 508 | } catch (parseError) { 509 | if (loadingIndicator) stopLoadingIndicator(loadingIndicator); 510 | reportLog( 511 | `Error parsing complexity analysis JSON: ${parseError.message}`, 512 | 'error' 513 | ); 514 | if (outputFormat === 'text') { 515 | console.error( 516 | chalk.red( 517 | `Error parsing complexity analysis JSON: ${parseError.message}` 518 | ) 519 | ); 520 | } 521 | throw parseError; 522 | } 523 | 524 | const taskIds = tasksData.tasks.map((t) => t.id); 525 | const analysisTaskIds = complexityAnalysis.map((a) => a.taskId); 526 | const missingTaskIds = taskIds.filter( 527 | (id) => !analysisTaskIds.includes(id) 528 | ); 529 | 530 | if (missingTaskIds.length > 0) { 531 | reportLog( 532 | `Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}`, 533 | 'warn' 534 | ); 535 | if (outputFormat === 'text') { 536 | console.log( 537 | chalk.yellow( 538 | `Missing analysis for ${missingTaskIds.length} tasks: ${missingTaskIds.join(', ')}` 539 | ) 540 | ); 541 | } 542 | for (const missingId of missingTaskIds) { 543 | const missingTask = tasksData.tasks.find((t) => t.id === missingId); 544 | if (missingTask) { 545 | reportLog(`Adding default analysis for task ${missingId}`, 'info'); 546 | complexityAnalysis.push({ 547 | taskId: missingId, 548 | taskTitle: missingTask.title, 549 | complexityScore: 5, 550 | recommendedSubtasks: 3, 551 | expansionPrompt: `Break down this task with a focus on ${missingTask.title.toLowerCase()}.`, 552 | reasoning: 553 | 'Automatically added due to missing analysis in AI response.' 554 | }); 555 | } 556 | } 557 | } 558 | 559 | // Merge with existing report - only keep entries from the current tag 560 | let finalComplexityAnalysis = []; 561 | 562 | if (existingReport && Array.isArray(existingReport.complexityAnalysis)) { 563 | // Create a map of task IDs that we just analyzed 564 | const analyzedTaskIds = new Set( 565 | complexityAnalysis.map((item) => item.taskId) 566 | ); 567 | 568 | // Keep existing entries that weren't in this analysis run AND belong to the current tag 569 | // We determine tag membership by checking if the task ID exists in the current tag's tasks 570 | const currentTagTaskIds = new Set(tasksData.tasks.map((t) => t.id)); 571 | const existingEntriesNotAnalyzed = 572 | existingReport.complexityAnalysis.filter( 573 | (item) => 574 | !analyzedTaskIds.has(item.taskId) && 575 | currentTagTaskIds.has(item.taskId) // Only keep entries for tasks in current tag 576 | ); 577 | 578 | // Combine with new analysis 579 | finalComplexityAnalysis = [ 580 | ...existingEntriesNotAnalyzed, 581 | ...complexityAnalysis 582 | ]; 583 | 584 | reportLog( 585 | `Merged ${complexityAnalysis.length} new analyses with ${existingEntriesNotAnalyzed.length} existing entries from current tag`, 586 | 'info' 587 | ); 588 | } else { 589 | // No existing report or invalid format, just use the new analysis 590 | finalComplexityAnalysis = complexityAnalysis; 591 | } 592 | 593 | const report = { 594 | meta: { 595 | generatedAt: new Date().toISOString(), 596 | tasksAnalyzed: tasksData.tasks.length, 597 | totalTasks: originalTaskCount, 598 | analysisCount: finalComplexityAnalysis.length, 599 | thresholdScore: thresholdScore, 600 | projectName: getProjectName(session), 601 | usedResearch: useResearch 602 | }, 603 | complexityAnalysis: finalComplexityAnalysis 604 | }; 605 | reportLog(`Writing complexity report to ${outputPath}...`, 'info'); 606 | fs.writeFileSync(outputPath, JSON.stringify(report, null, '\t'), 'utf8'); 607 | 608 | reportLog( 609 | `Task complexity analysis complete. Report written to ${outputPath}`, 610 | 'success' 611 | ); 612 | 613 | if (outputFormat === 'text') { 614 | console.log( 615 | chalk.green( 616 | `Task complexity analysis complete. Report written to ${outputPath}` 617 | ) 618 | ); 619 | // Calculate statistics specifically for this analysis run 620 | const highComplexity = complexityAnalysis.filter( 621 | (t) => t.complexityScore >= 8 622 | ).length; 623 | const mediumComplexity = complexityAnalysis.filter( 624 | (t) => t.complexityScore >= 5 && t.complexityScore < 8 625 | ).length; 626 | const lowComplexity = complexityAnalysis.filter( 627 | (t) => t.complexityScore < 5 628 | ).length; 629 | const totalAnalyzed = complexityAnalysis.length; 630 | 631 | console.log('\nCurrent Analysis Summary:'); 632 | console.log('----------------------------'); 633 | console.log(`Tasks analyzed in this run: ${totalAnalyzed}`); 634 | console.log(`High complexity tasks: ${highComplexity}`); 635 | console.log(`Medium complexity tasks: ${mediumComplexity}`); 636 | console.log(`Low complexity tasks: ${lowComplexity}`); 637 | 638 | if (existingReport) { 639 | console.log('\nUpdated Report Summary:'); 640 | console.log('----------------------------'); 641 | console.log( 642 | `Total analyses in report: ${finalComplexityAnalysis.length}` 643 | ); 644 | console.log( 645 | `Analyses from previous runs: ${finalComplexityAnalysis.length - totalAnalyzed}` 646 | ); 647 | console.log(`New/updated analyses: ${totalAnalyzed}`); 648 | } 649 | 650 | console.log(`Research-backed analysis: ${useResearch ? 'Yes' : 'No'}`); 651 | console.log( 652 | `\nSee ${outputPath} for the full report and expansion commands.` 653 | ); 654 | 655 | console.log( 656 | boxen( 657 | chalk.white.bold('Suggested Next Steps:') + 658 | '\n\n' + 659 | `${chalk.cyan('1.')} Run ${chalk.yellow('task-master complexity-report')} to review detailed findings\n` + 660 | `${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down complex tasks\n` + 661 | `${chalk.cyan('3.')} Run ${chalk.yellow('task-master expand --all')} to expand all pending tasks based on complexity`, 662 | { 663 | padding: 1, 664 | borderColor: 'cyan', 665 | borderStyle: 'round', 666 | margin: { top: 1 } 667 | } 668 | ) 669 | ); 670 | 671 | if (getDebugFlag(session)) { 672 | console.debug( 673 | chalk.gray( 674 | `Final analysis object: ${JSON.stringify(report, null, 2)}` 675 | ) 676 | ); 677 | } 678 | 679 | if (aiServiceResponse.telemetryData) { 680 | displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); 681 | } 682 | } 683 | 684 | return { 685 | report: report, 686 | telemetryData: aiServiceResponse?.telemetryData, 687 | tagInfo: aiServiceResponse?.tagInfo 688 | }; 689 | } catch (aiError) { 690 | if (loadingIndicator) stopLoadingIndicator(loadingIndicator); 691 | reportLog(`Error during AI service call: ${aiError.message}`, 'error'); 692 | if (outputFormat === 'text') { 693 | console.error( 694 | chalk.red(`Error during AI service call: ${aiError.message}`) 695 | ); 696 | if (aiError.message.includes('API key')) { 697 | console.log( 698 | chalk.yellow( 699 | '\nPlease ensure your API keys are correctly configured in .env or ~/.taskmaster/.env' 700 | ) 701 | ); 702 | console.log( 703 | chalk.yellow("Run 'task-master models --setup' if needed.") 704 | ); 705 | } 706 | } 707 | throw aiError; 708 | } 709 | } catch (error) { 710 | reportLog(`Error analyzing task complexity: ${error.message}`, 'error'); 711 | if (outputFormat === 'text') { 712 | console.error( 713 | chalk.red(`Error analyzing task complexity: ${error.message}`) 714 | ); 715 | if (getDebugFlag(session)) { 716 | console.error(error); 717 | } 718 | process.exit(1); 719 | } else { 720 | throw error; 721 | } 722 | } 723 | } 724 | 725 | export default analyzeTaskComplexity; 726 | ```