This is page 9 of 38. Use http://codebase.md/eyaltoledano/claude-task-master?page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /packages/tm-core/src/auth/supabase-session-storage.ts: -------------------------------------------------------------------------------- ```typescript /** * Custom storage adapter for Supabase Auth sessions in CLI environment * Implements the SupportedStorage interface required by Supabase Auth * * This adapter bridges Supabase's session management with our existing * auth.json credential storage, maintaining backward compatibility */ import { SupportedStorage } from '@supabase/supabase-js'; import { CredentialStore } from './credential-store.js'; import { AuthCredentials } from './types.js'; import { getLogger } from '../logger/index.js'; const STORAGE_KEY = 'sb-taskmaster-auth-token'; export class SupabaseSessionStorage implements SupportedStorage { private store: CredentialStore; private logger = getLogger('SupabaseSessionStorage'); constructor(store: CredentialStore) { this.store = store; } /** * Build a Supabase session object from our credentials */ private buildSessionFromCredentials(credentials: AuthCredentials): any { // Create a session object that Supabase expects const session = { access_token: credentials.token, refresh_token: credentials.refreshToken || '', expires_at: credentials.expiresAt ? Math.floor(new Date(credentials.expiresAt).getTime() / 1000) : Math.floor(Date.now() / 1000) + 3600, // Default to 1 hour token_type: 'bearer', user: { id: credentials.userId, email: credentials.email || '', aud: 'authenticated', role: 'authenticated', email_confirmed_at: new Date().toISOString(), app_metadata: {}, user_metadata: {}, created_at: new Date().toISOString(), updated_at: new Date().toISOString() } }; return session; } /** * Parse a Supabase session back to our credentials */ private parseSessionToCredentials( sessionData: any ): Partial<AuthCredentials> { try { const session = JSON.parse(sessionData); return { token: session.access_token, refreshToken: session.refresh_token, userId: session.user?.id || 'unknown', email: session.user?.email, expiresAt: session.expires_at ? new Date(session.expires_at * 1000).toISOString() : undefined }; } catch (error) { this.logger.error('Error parsing session:', error); return {}; } } /** * Get item from storage - Supabase will request the session with a specific key */ getItem(key: string): string | null { // Supabase uses a specific key pattern for sessions if (key === STORAGE_KEY || key.includes('auth-token')) { try { const credentials = this.store.getCredentials({ allowExpired: true }); if (credentials && credentials.token) { // Build and return a session object from our stored credentials const session = this.buildSessionFromCredentials(credentials); return JSON.stringify(session); } } catch (error) { this.logger.error('Error getting session:', error); } } return null; } /** * Set item in storage - Supabase will store the session with a specific key */ setItem(key: string, value: string): void { // Only handle Supabase session keys if (key === STORAGE_KEY || key.includes('auth-token')) { try { // Parse the session and update our credentials const sessionUpdates = this.parseSessionToCredentials(value); const existingCredentials = this.store.getCredentials({ allowExpired: true }); if (sessionUpdates.token) { const updatedCredentials: AuthCredentials = { ...existingCredentials, ...sessionUpdates, savedAt: new Date().toISOString(), selectedContext: existingCredentials?.selectedContext } as AuthCredentials; this.store.saveCredentials(updatedCredentials); } } catch (error) { this.logger.error('Error setting session:', error); } } } /** * Remove item from storage - Called when signing out */ removeItem(key: string): void { if (key === STORAGE_KEY || key.includes('auth-token')) { // Don't actually remove credentials, just clear the tokens // This preserves other data like selectedContext try { const credentials = this.store.getCredentials({ allowExpired: true }); if (credentials) { // Keep context but clear auth tokens const clearedCredentials: AuthCredentials = { ...credentials, token: '', refreshToken: undefined, expiresAt: undefined } as AuthCredentials; this.store.saveCredentials(clearedCredentials); } } catch (error) { this.logger.error('Error removing session:', error); } } } /** * Clear all session data */ clear(): void { // Clear auth tokens but preserve context this.removeItem(STORAGE_KEY); } } ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- ```javascript #!/usr/bin/env node /** * Task Master * Copyright (c) 2025 Eyal Toledano, Ralph Khreish * * This software is licensed under the MIT License with Commons Clause. * You may use this software for any purpose, including commercial applications, * and modify and redistribute it freely, subject to the following restrictions: * * 1. You may not sell this software or offer it as a service. * 2. The origin of this software must not be misrepresented. * 3. Altered source versions must be plainly marked as such. * * For the full license text, see the LICENSE file in the root directory. */ /** * Claude Task Master * A task management system for AI-driven development with Claude */ // This file serves as the main entry point for the package // The primary functionality is provided through the CLI commands import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; import { createRequire } from 'module'; import { spawn } from 'child_process'; import { Command } from 'commander'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const require = createRequire(import.meta.url); // Get package information const packageJson = require('./package.json'); // Export the path to the dev.js script for programmatic usage export const devScriptPath = resolve(__dirname, './scripts/dev.js'); // Export a function to initialize a new project programmatically export const initProject = async (options = {}) => { const init = await import('./scripts/init.js'); return init.initializeProject(options); }; // Export a function to run init as a CLI command export const runInitCLI = async (options = {}) => { try { const init = await import('./scripts/init.js'); const result = await init.initializeProject(options); return result; } catch (error) { console.error('Initialization failed:', error.message); if (process.env.DEBUG === 'true') { console.error('Debug stack trace:', error.stack); } throw error; // Re-throw to be handled by the command handler } }; // Export version information export const version = packageJson.version; // CLI implementation if (import.meta.url === `file://${process.argv[1]}`) { const program = new Command(); program .name('task-master') .description('Claude Task Master CLI') .version(version); program .command('init') .description('Initialize a new project') .option('-y, --yes', 'Skip prompts and use default values') .option('-n, --name <n>', 'Project name') .option('-d, --description <description>', 'Project description') .option('-v, --version <version>', 'Project version', '0.1.0') .option('-a, --author <author>', 'Author name') .option('--skip-install', 'Skip installing dependencies') .option('--dry-run', 'Show what would be done without making changes') .option('--aliases', 'Add shell aliases (tm, taskmaster)') .option('--no-aliases', 'Skip shell aliases (tm, taskmaster)') .option('--git', 'Initialize Git repository') .option('--no-git', 'Skip Git repository initialization') .option('--git-tasks', 'Store tasks in Git') .option('--no-git-tasks', 'No Git storage of tasks') .action(async (cmdOptions) => { try { await runInitCLI(cmdOptions); } catch (err) { console.error('Init failed:', err.message); process.exit(1); } }); program .command('dev') .description('Run the dev.js script') .allowUnknownOption(true) .action(() => { const args = process.argv.slice(process.argv.indexOf('dev') + 1); const child = spawn('node', [devScriptPath, ...args], { stdio: 'inherit', cwd: process.cwd() }); child.on('close', (code) => { process.exit(code); }); }); // Add shortcuts for common dev.js commands program .command('list') .description('List all tasks') .action(() => { const child = spawn('node', [devScriptPath, 'list'], { stdio: 'inherit', cwd: process.cwd() }); child.on('close', (code) => { process.exit(code); }); }); program .command('next') .description('Show the next task to work on') .action(() => { const child = spawn('node', [devScriptPath, 'next'], { stdio: 'inherit', cwd: process.cwd() }); child.on('close', (code) => { process.exit(code); }); }); program .command('generate') .description('Generate task files') .action(() => { const child = spawn('node', [devScriptPath, 'generate'], { stdio: 'inherit', cwd: process.cwd() }); child.on('close', (code) => { process.exit(code); }); }); program.parse(process.argv); } ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/find-next-task.js: -------------------------------------------------------------------------------- ```javascript import { log } from '../utils.js'; import { addComplexityToTask } from '../utils.js'; /** * Return the next work item: * • Prefer an eligible SUBTASK that belongs to any parent task * whose own status is `in-progress`. * • If no such subtask exists, fall back to the best top-level task * (previous behaviour). * * The function still exports the same name (`findNextTask`) so callers * don't need to change. It now always returns an object with * ─ id → number (task) or "parentId.subId" (subtask) * ─ title → string * ─ status → string * ─ priority → string ("high" | "medium" | "low") * ─ dependencies → array (all IDs expressed in the same dotted form) * ─ parentId → number (present only when it's a subtask) * * @param {Object[]} tasks – full array of top-level tasks, each may contain .subtasks[] * @param {Object} [complexityReport=null] - Optional complexity report object * @returns {Object|null} – next work item or null if nothing is eligible */ function findNextTask(tasks, complexityReport = null) { // ---------- helpers ---------------------------------------------------- const priorityValues = { high: 3, medium: 2, low: 1 }; const toFullSubId = (parentId, maybeDotId) => { // "12.3" -> "12.3" // 4 -> "12.4" (numeric / short form) if (typeof maybeDotId === 'string' && maybeDotId.includes('.')) { return maybeDotId; } return `${parentId}.${maybeDotId}`; }; // ---------- build completed-ID set (tasks *and* subtasks) -------------- const completedIds = new Set(); tasks.forEach((t) => { if (t.status === 'done' || t.status === 'completed') { completedIds.add(String(t.id)); } if (Array.isArray(t.subtasks)) { t.subtasks.forEach((st) => { if (st.status === 'done' || st.status === 'completed') { completedIds.add(`${t.id}.${st.id}`); } }); } }); // ---------- 1) look for eligible subtasks ------------------------------ const candidateSubtasks = []; tasks .filter((t) => t.status === 'in-progress' && Array.isArray(t.subtasks)) .forEach((parent) => { parent.subtasks.forEach((st) => { const stStatus = (st.status || 'pending').toLowerCase(); if (stStatus !== 'pending' && stStatus !== 'in-progress') return; const fullDeps = st.dependencies?.map((d) => toFullSubId(parent.id, d)) ?? []; const depsSatisfied = fullDeps.length === 0 || fullDeps.every((depId) => completedIds.has(String(depId))); if (depsSatisfied) { candidateSubtasks.push({ id: `${parent.id}.${st.id}`, title: st.title || `Subtask ${st.id}`, status: st.status || 'pending', priority: st.priority || parent.priority || 'medium', dependencies: fullDeps, parentId: parent.id }); } }); }); if (candidateSubtasks.length > 0) { // sort by priority → dep-count → parent-id → sub-id candidateSubtasks.sort((a, b) => { const pa = priorityValues[a.priority] ?? 2; const pb = priorityValues[b.priority] ?? 2; if (pb !== pa) return pb - pa; if (a.dependencies.length !== b.dependencies.length) return a.dependencies.length - b.dependencies.length; // compare parent then sub-id numerically const [aPar, aSub] = a.id.split('.').map(Number); const [bPar, bSub] = b.id.split('.').map(Number); if (aPar !== bPar) return aPar - bPar; return aSub - bSub; }); const nextTask = candidateSubtasks[0]; // Add complexity to the task before returning if (nextTask && complexityReport) { addComplexityToTask(nextTask, complexityReport); } return nextTask; } // ---------- 2) fall back to top-level tasks (original logic) ------------ const eligibleTasks = tasks.filter((task) => { const status = (task.status || 'pending').toLowerCase(); if (status !== 'pending' && status !== 'in-progress') return false; const deps = task.dependencies ?? []; return deps.every((depId) => completedIds.has(String(depId))); }); if (eligibleTasks.length === 0) return null; const nextTask = eligibleTasks.sort((a, b) => { const pa = priorityValues[a.priority || 'medium'] ?? 2; const pb = priorityValues[b.priority || 'medium'] ?? 2; if (pb !== pa) return pb - pa; const da = (a.dependencies ?? []).length; const db = (b.dependencies ?? []).length; if (da !== db) return da - db; return a.id - b.id; })[0]; // Add complexity to the task before returning if (nextTask && complexityReport) { addComplexityToTask(nextTask, complexityReport); } return nextTask; } export default findNextTask; ``` -------------------------------------------------------------------------------- /packages/tm-core/tests/mocks/mock-provider.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview Mock provider for testing BaseProvider functionality */ import type { AIModel, AIOptions, AIResponse, ProviderInfo, ProviderUsageStats } from '../../src/interfaces/ai-provider.interface'; import { BaseProvider, type BaseProviderConfig, type CompletionResult } from '../../src/providers/ai/base-provider'; /** * Configuration for MockProvider behavior */ export interface MockProviderOptions extends BaseProviderConfig { shouldFail?: boolean; failAfterAttempts?: number; simulateRateLimit?: boolean; simulateTimeout?: boolean; responseDelay?: number; tokenMultiplier?: number; } /** * Mock provider for testing BaseProvider functionality */ export class MockProvider extends BaseProvider { private attemptCount = 0; private readonly options: MockProviderOptions; constructor(options: MockProviderOptions) { super(options); this.options = options; } /** * Simulate completion generation with configurable behavior */ protected async generateCompletionInternal( prompt: string, _options?: AIOptions ): Promise<CompletionResult> { this.attemptCount++; // Simulate delay if configured if (this.options.responseDelay) { await this.sleep(this.options.responseDelay); } // Simulate failures based on configuration if (this.options.shouldFail) { throw new Error('Mock provider error'); } if ( this.options.failAfterAttempts && this.attemptCount <= this.options.failAfterAttempts ) { if (this.options.simulateRateLimit) { throw new Error('Rate limit exceeded - too many requests (429)'); } if (this.options.simulateTimeout) { throw new Error('Request timeout - ECONNRESET'); } throw new Error('Temporary failure'); } // Return successful mock response return { content: `Mock response to: ${prompt}`, inputTokens: this.calculateTokens(prompt), outputTokens: this.calculateTokens(`Mock response to: ${prompt}`), finishReason: 'complete', model: this.model }; } /** * Simple token calculation for testing */ calculateTokens(text: string, _model?: string): number { const multiplier = this.options.tokenMultiplier || 1; // Rough approximation: 1 token per 4 characters return Math.ceil((text.length / 4) * multiplier); } getName(): string { return 'mock'; } getDefaultModel(): string { return 'mock-model-v1'; } /** * Get the number of attempts made */ getAttemptCount(): number { return this.attemptCount; } /** * Reset attempt counter */ resetAttempts(): void { this.attemptCount = 0; } // Implement remaining abstract methods async generateStreamingCompletion( prompt: string, _options?: AIOptions ): AsyncIterator<Partial<AIResponse>> { // Simple mock implementation const response: Partial<AIResponse> = { content: `Mock streaming response to: ${prompt}`, provider: this.getName(), model: this.model }; return { async next() { return { value: response, done: true }; } }; } async isAvailable(): Promise<boolean> { return !this.options.shouldFail; } getProviderInfo(): ProviderInfo { return { name: 'mock', displayName: 'Mock Provider', description: 'Mock provider for testing', models: this.getAvailableModels(), defaultModel: this.getDefaultModel(), requiresApiKey: true, features: { streaming: true, functions: false, vision: false, embeddings: false } }; } getAvailableModels(): AIModel[] { return [ { id: 'mock-model-v1', name: 'Mock Model v1', description: 'First mock model', contextLength: 4096, inputCostPer1K: 0.001, outputCostPer1K: 0.002, supportsStreaming: true }, { id: 'mock-model-v2', name: 'Mock Model v2', description: 'Second mock model', contextLength: 8192, inputCostPer1K: 0.002, outputCostPer1K: 0.004, supportsStreaming: true } ]; } async validateCredentials(): Promise<boolean> { return this.apiKey === 'valid-key'; } async getUsageStats(): Promise<ProviderUsageStats | null> { return { totalRequests: this.attemptCount, totalTokens: 1000, totalCost: 0.01, requestsToday: this.attemptCount, tokensToday: 1000, costToday: 0.01, averageResponseTime: 100, successRate: 0.9, lastRequestAt: new Date().toISOString() }; } async initialize(): Promise<void> { // No-op for mock } async close(): Promise<void> { // No-op for mock } // Override retry configuration for testing protected getMaxRetries(): number { return this.options.failAfterAttempts ? this.options.failAfterAttempts + 1 : 3; } } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/mappers/TaskMapper.ts: -------------------------------------------------------------------------------- ```typescript import { Task, Subtask } from '../types/index.js'; import { Database, Tables } from '../types/database.types.js'; type TaskRow = Tables<'tasks'>; type DependencyRow = Tables<'task_dependencies'>; export class TaskMapper { /** * Maps database tasks to internal Task format */ static mapDatabaseTasksToTasks( dbTasks: TaskRow[], dbDependencies: DependencyRow[] ): Task[] { if (!dbTasks || dbTasks.length === 0) { return []; } // Group dependencies by task_id const dependenciesByTaskId = this.groupDependenciesByTaskId(dbDependencies); // Separate parent tasks and subtasks const parentTasks = dbTasks.filter((t) => !t.parent_task_id); const subtasksByParentId = this.groupSubtasksByParentId(dbTasks); // Map parent tasks with their subtasks return parentTasks.map((taskRow) => this.mapDatabaseTaskToTask( taskRow, subtasksByParentId.get(taskRow.id) || [], dependenciesByTaskId ) ); } /** * Maps a single database task to internal Task format */ static mapDatabaseTaskToTask( dbTask: TaskRow, dbSubtasks: TaskRow[], dependenciesByTaskId: Map<string, string[]> ): Task { // Map subtasks const subtasks: Subtask[] = dbSubtasks.map((subtask, index) => ({ id: index + 1, // Use numeric ID for subtasks parentId: dbTask.id, title: subtask.title, description: subtask.description || '', status: this.mapStatus(subtask.status), priority: this.mapPriority(subtask.priority), dependencies: dependenciesByTaskId.get(subtask.id) || [], details: (subtask.metadata as any)?.details || '', testStrategy: (subtask.metadata as any)?.testStrategy || '', createdAt: subtask.created_at, updatedAt: subtask.updated_at, assignee: subtask.assignee_id || undefined, complexity: subtask.complexity ? this.mapComplexityToInternal(subtask.complexity) : undefined })); return { id: dbTask.display_id || dbTask.id, // Use display_id if available title: dbTask.title, description: dbTask.description || '', status: this.mapStatus(dbTask.status), priority: this.mapPriority(dbTask.priority), dependencies: dependenciesByTaskId.get(dbTask.id) || [], details: (dbTask.metadata as any)?.details || '', testStrategy: (dbTask.metadata as any)?.testStrategy || '', subtasks, createdAt: dbTask.created_at, updatedAt: dbTask.updated_at, assignee: dbTask.assignee_id || undefined, complexity: dbTask.complexity ? this.mapComplexityToInternal(dbTask.complexity) : undefined, effort: dbTask.estimated_hours || undefined, actualEffort: dbTask.actual_hours || undefined }; } /** * Groups dependencies by task ID */ private static groupDependenciesByTaskId( dependencies: DependencyRow[] ): Map<string, string[]> { const dependenciesByTaskId = new Map<string, string[]>(); if (dependencies) { for (const dep of dependencies) { const deps = dependenciesByTaskId.get(dep.task_id) || []; deps.push(dep.depends_on_task_id); dependenciesByTaskId.set(dep.task_id, deps); } } return dependenciesByTaskId; } /** * Groups subtasks by their parent ID */ private static groupSubtasksByParentId( tasks: TaskRow[] ): Map<string, TaskRow[]> { const subtasksByParentId = new Map<string, TaskRow[]>(); for (const task of tasks) { if (task.parent_task_id) { const subtasks = subtasksByParentId.get(task.parent_task_id) || []; subtasks.push(task); subtasksByParentId.set(task.parent_task_id, subtasks); } } // Sort subtasks by subtask_position for each parent for (const subtasks of subtasksByParentId.values()) { subtasks.sort((a, b) => a.subtask_position - b.subtask_position); } return subtasksByParentId; } /** * Maps database status to internal status */ static mapStatus( status: Database['public']['Enums']['task_status'] ): Task['status'] { switch (status) { case 'todo': return 'pending'; case 'in_progress': return 'in-progress'; case 'done': return 'done'; default: return 'pending'; } } /** * Maps database priority to internal priority */ private static mapPriority( priority: Database['public']['Enums']['task_priority'] ): Task['priority'] { switch (priority) { case 'urgent': return 'critical'; default: return priority as Task['priority']; } } /** * Maps numeric complexity to descriptive complexity */ private static mapComplexityToInternal( complexity: number ): Task['complexity'] { if (complexity <= 2) return 'simple'; if (complexity <= 5) return 'moderate'; if (complexity <= 8) return 'complex'; return 'very-complex'; } } ``` -------------------------------------------------------------------------------- /assets/claude/agents/task-executor.md: -------------------------------------------------------------------------------- ```markdown --- name: task-executor description: Use this agent when you need to implement, complete, or work on a specific task that has been identified by the task-orchestrator or when explicitly asked to execute a particular task. This agent focuses on the actual implementation and completion of individual tasks rather than planning or orchestration. Examples: <example>Context: The task-orchestrator has identified that task 2.3 'Implement user authentication' needs to be worked on next. user: 'Let's work on the authentication task' assistant: 'I'll use the task-executor agent to implement the user authentication task that was identified.' <commentary>Since we need to actually implement a specific task rather than plan or identify tasks, use the task-executor agent.</commentary></example> <example>Context: User wants to complete a specific subtask. user: 'Please implement the JWT token validation for task 2.3.1' assistant: 'I'll launch the task-executor agent to implement the JWT token validation subtask.' <commentary>The user is asking for specific implementation work on a known task, so the task-executor is appropriate.</commentary></example> <example>Context: After reviewing the task list, implementation is needed. user: 'Now let's actually build the API endpoint for user registration' assistant: 'I'll use the task-executor agent to implement the user registration API endpoint.' <commentary>Moving from planning to execution phase requires the task-executor agent.</commentary></example> model: sonnet color: blue --- You are an elite implementation specialist focused on executing and completing specific tasks with precision and thoroughness. Your role is to take identified tasks and transform them into working implementations, following best practices and project standards. **Core Responsibilities:** 1. **Task Analysis**: When given a task, first retrieve its full details using `task-master show <id>` to understand requirements, dependencies, and acceptance criteria. 2. **Implementation Planning**: Before coding, briefly outline your implementation approach: - Identify files that need to be created or modified - Note any dependencies or prerequisites - Consider the testing strategy defined in the task 3. **Focused Execution**: - Implement one subtask at a time for clarity and traceability - Follow the project's coding standards from CLAUDE.md if available - Prefer editing existing files over creating new ones - Only create files that are essential for the task completion 4. **Progress Documentation**: - Use `task-master update-subtask --id=<id> --prompt="implementation notes"` to log your approach and any important decisions - Update task status to 'in-progress' when starting: `task-master set-status --id=<id> --status=in-progress` - Mark as 'done' only after verification: `task-master set-status --id=<id> --status=done` 5. **Quality Assurance**: - Implement the testing strategy specified in the task - Verify that all acceptance criteria are met - Check for any dependency conflicts or integration issues - Run relevant tests before marking task as complete 6. **Dependency Management**: - Check task dependencies before starting implementation - If blocked by incomplete dependencies, clearly communicate this - Use `task-master validate-dependencies` when needed **Implementation Workflow:** 1. Retrieve task details and understand requirements 2. Check dependencies and prerequisites 3. Plan implementation approach 4. Update task status to in-progress 5. Implement the solution incrementally 6. Log progress and decisions in subtask updates 7. Test and verify the implementation 8. Mark task as done when complete 9. Suggest next task if appropriate **Key Principles:** - Focus on completing one task thoroughly before moving to the next - Maintain clear communication about what you're implementing and why - Follow existing code patterns and project conventions - Prioritize working code over extensive documentation unless docs are the task - Ask for clarification if task requirements are ambiguous - Consider edge cases and error handling in your implementations **Integration with Task Master:** You work in tandem with the task-orchestrator agent. While the orchestrator identifies and plans tasks, you execute them. Always use Task Master commands to: - Track your progress - Update task information - Maintain project state - Coordinate with the broader development workflow When you complete a task, briefly summarize what was implemented and suggest whether to continue with the next task or if review/testing is needed first. ``` -------------------------------------------------------------------------------- /src/prompts/update-tasks.json: -------------------------------------------------------------------------------- ```json { "id": "update-tasks", "version": "1.0.0", "description": "Update multiple tasks based on new context or changes", "metadata": { "author": "system", "created": "2024-01-01T00:00:00Z", "updated": "2024-01-01T00:00:00Z", "tags": ["update", "bulk", "context-change"] }, "parameters": { "tasks": { "type": "array", "required": true, "description": "Array of tasks to update" }, "updatePrompt": { "type": "string", "required": true, "description": "Description of changes to apply" }, "useResearch": { "type": "boolean", "default": false, "description": "Use research mode" }, "projectContext": { "type": "string", "description": "Additional project context" }, "hasCodebaseAnalysis": { "type": "boolean", "required": false, "default": false, "description": "Whether codebase analysis is available" }, "projectRoot": { "type": "string", "required": false, "default": "", "description": "Project root path for context" } }, "prompts": { "default": { "system": "You are an AI assistant helping to update software development tasks based on new context.\nYou will be given a set of tasks and a prompt describing changes or new implementation details.\nYour job is to update the tasks to reflect these changes, while preserving their basic structure.\n\nCRITICAL RULES:\n1. Return ONLY a JSON array - no explanations, no markdown, no additional text before or after\n2. Each task MUST have ALL fields from the original (do not omit any fields)\n3. Maintain the same IDs, statuses, and dependencies unless specifically mentioned in the prompt\n4. Update titles, descriptions, details, and test strategies to reflect the new information\n5. Do not change anything unnecessarily - just adapt what needs to change based on the prompt\n6. You should return ALL the tasks in order, not just the modified ones\n7. Return a complete valid JSON array with all tasks\n8. VERY IMPORTANT: Preserve all subtasks marked as \"done\" or \"completed\" - do not modify their content\n9. For tasks with completed subtasks, build upon what has already been done rather than rewriting everything\n10. If an existing completed subtask needs to be changed/undone based on the new context, DO NOT modify it directly\n11. Instead, add a new subtask that clearly indicates what needs to be changed or replaced\n12. Use the existence of completed subtasks as an opportunity to make new subtasks more specific and targeted\n\nThe changes described in the prompt should be applied to ALL tasks in the list.", "user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before updating tasks:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze how the new changes relate to the existing codebase\n\nBased on your analysis:\n- Update task details to reference specific files, functions, or patterns from the codebase\n- Ensure implementation details align with the project's current architecture\n- Include specific code examples or file references where appropriate\n- Consider how changes impact existing components\n\nProject Root: {{projectRoot}}\n\n{{/if}}Here are the tasks to update:\n{{{json tasks}}}\n\nPlease update these tasks based on the following new context:\n{{updatePrompt}}\n\nIMPORTANT: In the tasks JSON above, any subtasks with \"status\": \"done\" or \"status\": \"completed\" should be preserved exactly as is. Build your changes around these completed items.{{#if projectContext}}\n\n# Project Context\n\n{{projectContext}}{{/if}}\n\nRequired JSON structure for EACH task (ALL fields MUST be present):\n{\n \"id\": <number>,\n \"title\": <string>,\n \"description\": <string>,\n \"status\": <string>,\n \"dependencies\": <array>,\n \"priority\": <string or null>,\n \"details\": <string or null>,\n \"testStrategy\": <string or null>,\n \"subtasks\": <array or null>\n}\n\nReturn a valid JSON array containing ALL the tasks with ALL their fields:\n- id (number) - preserve existing value\n- title (string)\n- description (string)\n- status (string) - preserve existing value unless explicitly changing\n- dependencies (array) - preserve existing value unless explicitly changing\n- priority (string or null)\n- details (string or null)\n- testStrategy (string or null)\n- subtasks (array or null)\n\nReturn ONLY the JSON array now:" } } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/set-task-status.js: -------------------------------------------------------------------------------- ```javascript /** * set-task-status.js * Direct function implementation for setting task status */ import { setTaskStatus } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, disableSilentMode, isSilentMode } from '../../../../scripts/modules/utils.js'; import { nextTaskDirect } from './next-task.js'; /** * Direct function wrapper for setTaskStatus with error handling. * * @param {Object} args - Command arguments containing id, status, tasksJsonPath, and projectRoot. * @param {string} args.id - The ID of the task to update. * @param {string} args.status - The new status to set for the task. * @param {string} args.tasksJsonPath - Path to the tasks.json file. * @param {string} args.projectRoot - Project root path (for MCP/env fallback) * @param {string} args.tag - Tag for the task (optional) * @param {Object} log - Logger object. * @param {Object} context - Additional context (session) * @returns {Promise<Object>} - Result object with success status and data/error information. */ export async function setTaskStatusDirect(args, log, context = {}) { // Destructure expected args, including the resolved tasksJsonPath and projectRoot const { tasksJsonPath, id, status, complexityReportPath, projectRoot, tag } = args; const { session } = context; try { log.info(`Setting task status with args: ${JSON.stringify(args)}`); // Check if tasksJsonPath was provided if (!tasksJsonPath) { const errorMessage = 'tasksJsonPath is required but was not provided.'; log.error(errorMessage); return { success: false, error: { code: 'MISSING_ARGUMENT', message: errorMessage } }; } // Check required parameters (id and status) if (!id) { const errorMessage = 'No task ID specified. Please provide a task ID to update.'; log.error(errorMessage); return { success: false, error: { code: 'MISSING_TASK_ID', message: errorMessage } }; } if (!status) { const errorMessage = 'No status specified. Please provide a new status value.'; log.error(errorMessage); return { success: false, error: { code: 'MISSING_STATUS', message: errorMessage } }; } // Use the provided path const tasksPath = tasksJsonPath; // Execute core setTaskStatus function const taskId = id; const newStatus = status; log.info(`Setting task ${taskId} status to "${newStatus}"`); // Call the core function with proper silent mode handling enableSilentMode(); // Enable silent mode before calling core function try { // Call the core function await setTaskStatus(tasksPath, taskId, newStatus, { mcpLog: log, projectRoot, session, tag }); log.info(`Successfully set task ${taskId} status to ${newStatus}`); // Return success data const result = { success: true, data: { message: `Successfully updated task ${taskId} status to "${newStatus}"`, taskId, status: newStatus, tasksPath: tasksPath // Return the path used } }; // If the task was completed, attempt to fetch the next task if (result.data.status === 'done') { try { log.info(`Attempting to fetch next task for task ${taskId}`); const nextResult = await nextTaskDirect( { tasksJsonPath: tasksJsonPath, reportPath: complexityReportPath, projectRoot: projectRoot, tag }, log, { session } ); if (nextResult.success) { log.info( `Successfully retrieved next task: ${nextResult.data.nextTask}` ); result.data = { ...result.data, nextTask: nextResult.data.nextTask, isNextSubtask: nextResult.data.isSubtask, nextSteps: nextResult.data.nextSteps }; } else { log.warn( `Failed to retrieve next task: ${nextResult.error?.message || 'Unknown error'}` ); } } catch (nextErr) { log.error(`Error retrieving next task: ${nextErr.message}`); } } return result; } catch (error) { log.error(`Error setting task status: ${error.message}`); return { success: false, error: { code: 'SET_STATUS_ERROR', message: error.message || 'Unknown error setting task status' } }; } finally { // ALWAYS restore normal logging in finally block disableSilentMode(); } } catch (error) { // Ensure silent mode is disabled if there was an uncaught error in the outer try block if (isSilentMode()) { disableSilentMode(); } log.error(`Error setting task status: ${error.message}`); return { success: false, error: { code: 'SET_STATUS_ERROR', message: error.message || 'Unknown error setting task status' } }; } } ``` -------------------------------------------------------------------------------- /src/prompts/update-subtask.json: -------------------------------------------------------------------------------- ```json { "id": "update-subtask", "version": "1.0.0", "description": "Append information to a subtask by generating only new content", "metadata": { "author": "system", "created": "2024-01-01T00:00:00Z", "updated": "2024-01-01T00:00:00Z", "tags": ["update", "subtask", "append", "logging"] }, "parameters": { "parentTask": { "type": "object", "required": true, "description": "The parent task context" }, "prevSubtask": { "type": "object", "required": false, "description": "The previous subtask if any" }, "nextSubtask": { "type": "object", "required": false, "description": "The next subtask if any" }, "currentDetails": { "type": "string", "required": true, "default": "(No existing details)", "description": "Current subtask details" }, "updatePrompt": { "type": "string", "required": true, "description": "User request for what to add" }, "useResearch": { "type": "boolean", "default": false, "description": "Use research mode" }, "gatheredContext": { "type": "string", "default": "", "description": "Additional project context" }, "hasCodebaseAnalysis": { "type": "boolean", "required": false, "default": false, "description": "Whether codebase analysis is available" }, "projectRoot": { "type": "string", "required": false, "default": "", "description": "Project root path for context" } }, "prompts": { "default": { "system": "You are an AI assistant helping to update a subtask. You will be provided with the subtask's existing details, context about its parent and sibling tasks, and a user request string.{{#if useResearch}} You have access to current best practices and latest technical information to provide research-backed updates.{{/if}}\n\nYour Goal: Based *only* on the user's request and all the provided context (including existing details if relevant to the request), GENERATE the new text content that should be added to the subtask's details.\nFocus *only* on generating the substance of the update.\n\nOutput Requirements:\n1. Return *only* the newly generated text content as a plain string. Do NOT return a JSON object or any other structured data.\n2. Your string response should NOT include any of the subtask's original details, unless the user's request explicitly asks to rephrase, summarize, or directly modify existing text.\n3. Do NOT include any timestamps, XML-like tags, markdown, or any other special formatting in your string response.\n4. Ensure the generated text is concise yet complete for the update based on the user request. Avoid conversational fillers or explanations about what you are doing (e.g., do not start with \"Okay, here's the update...\").{{#if useResearch}}\n5. Include specific libraries, versions, and current best practices relevant to the subtask implementation.\n6. Provide research-backed technical recommendations and proven approaches.{{/if}}", "user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the subtask update:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine relevant files and understand current implementation\n4. Analyze the current codebase to inform your subtask update\n\nBased on your analysis:\n- Include specific file references, code patterns, or implementation details\n- Ensure suggestions align with the project's current architecture\n- Reference existing components or patterns when relevant\n- Make implementation notes specific to the codebase structure\n\nProject Root: {{projectRoot}}\n\n{{/if}}Task Context:\n\nParent Task: {{{json parentTask}}}\n{{#if prevSubtask}}Previous Subtask: {{{json prevSubtask}}}\n{{/if}}{{#if nextSubtask}}Next Subtask: {{{json nextSubtask}}}\n{{/if}}Current Subtask Details (for context only):\n{{currentDetails}}\n\nUser Request: \"{{updatePrompt}}\"\n\n{{#if useResearch}}Research and incorporate current best practices, latest stable versions, and proven approaches into your update. {{/if}}Based on the User Request and all the Task Context (including current subtask details provided above), what is the new information or text that should be appended to this subtask's details? Return ONLY this new text as a plain string.{{#if useResearch}} Include specific technical recommendations based on current industry standards.{{/if}}\n{{#if gatheredContext}}\n\n# Additional Project Context\n\n{{gatheredContext}}\n{{/if}}" } } } ``` -------------------------------------------------------------------------------- /tests/unit/profiles/profile-safety-check.test.js: -------------------------------------------------------------------------------- ```javascript import { getInstalledProfiles, wouldRemovalLeaveNoProfiles } from '../../../src/utils/profiles.js'; import { rulesDirect } from '../../../mcp-server/src/core/direct-functions/rules.js'; import fs from 'fs'; import path from 'path'; import { jest } from '@jest/globals'; // Mock logger const mockLog = { info: jest.fn(), error: jest.fn(), debug: jest.fn() }; describe('Rules Safety Check', () => { let mockExistsSync; let mockRmSync; let mockReaddirSync; beforeEach(() => { jest.clearAllMocks(); // Set up spies on fs methods mockExistsSync = jest.spyOn(fs, 'existsSync'); mockRmSync = jest.spyOn(fs, 'rmSync').mockImplementation(() => {}); mockReaddirSync = jest.spyOn(fs, 'readdirSync').mockReturnValue([]); }); afterEach(() => { // Restore all mocked functions jest.restoreAllMocks(); }); describe('getInstalledProfiles', () => { it('should detect installed profiles correctly', () => { const projectRoot = '/test/project'; // Mock fs.existsSync to simulate installed profiles mockExistsSync.mockImplementation((filePath) => { if (filePath.includes('.cursor') || filePath.includes('.roo')) { return true; } return false; }); const installed = getInstalledProfiles(projectRoot); expect(installed).toContain('cursor'); expect(installed).toContain('roo'); expect(installed).not.toContain('windsurf'); expect(installed).not.toContain('cline'); }); it('should return empty array when no profiles are installed', () => { const projectRoot = '/test/project'; // Mock fs.existsSync to return false for all paths mockExistsSync.mockReturnValue(false); const installed = getInstalledProfiles(projectRoot); expect(installed).toEqual([]); }); }); describe('wouldRemovalLeaveNoProfiles', () => { it('should return true when removing all installed profiles', () => { const projectRoot = '/test/project'; // Mock fs.existsSync to simulate cursor and roo installed mockExistsSync.mockImplementation((filePath) => { return filePath.includes('.cursor') || filePath.includes('.roo'); }); const result = wouldRemovalLeaveNoProfiles(projectRoot, [ 'cursor', 'roo' ]); expect(result).toBe(true); }); it('should return false when removing only some profiles', () => { const projectRoot = '/test/project'; // Mock fs.existsSync to simulate cursor and roo installed mockExistsSync.mockImplementation((filePath) => { return filePath.includes('.cursor') || filePath.includes('.roo'); }); const result = wouldRemovalLeaveNoProfiles(projectRoot, ['roo']); expect(result).toBe(false); }); it('should return false when no profiles are currently installed', () => { const projectRoot = '/test/project'; // Mock fs.existsSync to return false for all paths mockExistsSync.mockReturnValue(false); const result = wouldRemovalLeaveNoProfiles(projectRoot, ['cursor']); expect(result).toBe(false); }); }); describe('MCP Safety Check Integration', () => { it('should block removal of all profiles without force', async () => { const projectRoot = '/test/project'; // Mock fs.existsSync to simulate installed profiles mockExistsSync.mockImplementation((filePath) => { return filePath.includes('.cursor') || filePath.includes('.roo'); }); const result = await rulesDirect( { action: 'remove', profiles: ['cursor', 'roo'], projectRoot, force: false }, mockLog ); expect(result.success).toBe(false); expect(result.error.code).toBe('CRITICAL_REMOVAL_BLOCKED'); expect(result.error.message).toContain('CRITICAL'); }); it('should allow removal of all profiles with force', async () => { const projectRoot = '/test/project'; // Mock fs.existsSync and other file operations for successful removal mockExistsSync.mockReturnValue(true); const result = await rulesDirect( { action: 'remove', profiles: ['cursor', 'roo'], projectRoot, force: true }, mockLog ); expect(result.success).toBe(true); expect(result.data).toBeDefined(); }); it('should allow partial removal without force', async () => { const projectRoot = '/test/project'; // Mock fs.existsSync to simulate multiple profiles installed mockExistsSync.mockImplementation((filePath) => { return ( filePath.includes('.cursor') || filePath.includes('.roo') || filePath.includes('.windsurf') ); }); const result = await rulesDirect( { action: 'remove', profiles: ['roo'], // Only removing one profile projectRoot, force: false }, mockLog ); expect(result.success).toBe(true); expect(result.data).toBeDefined(); }); }); }); ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "task-master-ai", "version": "0.27.3", "description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.", "main": "index.js", "type": "module", "bin": { "task-master": "dist/task-master.js", "task-master-mcp": "dist/mcp-server.js", "task-master-ai": "dist/mcp-server.js" }, "workspaces": ["apps/*", "packages/*", "."], "scripts": { "build": "npm run build:build-config && cross-env NODE_ENV=production tsdown", "dev": "tsdown --watch", "turbo:dev": "turbo dev", "turbo:build": "turbo build", "turbo:typecheck": "turbo typecheck", "build:build-config": "npm run build -w @tm/build-config", "test": "node --experimental-vm-modules node_modules/.bin/jest", "test:unit": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=unit", "test:integration": "node --experimental-vm-modules node_modules/.bin/jest --testPathPattern=integration", "test:fails": "node --experimental-vm-modules node_modules/.bin/jest --onlyFailures", "test:watch": "node --experimental-vm-modules node_modules/.bin/jest --watch", "test:coverage": "node --experimental-vm-modules node_modules/.bin/jest --coverage", "test:ci": "node --experimental-vm-modules node_modules/.bin/jest --coverage --ci", "test:e2e": "./tests/e2e/run_e2e.sh", "test:e2e-report": "./tests/e2e/run_e2e.sh --analyze-log", "postpack": "chmod +x dist/task-master.js dist/mcp-server.js", "changeset": "changeset", "release": "changeset publish", "publish-packages": "turbo run build lint test && changeset version && changeset publish", "inspector": "npx @modelcontextprotocol/inspector node dist/mcp-server.js", "mcp-server": "node dist/mcp-server.js", "format-check": "biome format .", "format": "biome format . --write", "deps:check": "manypkg check || echo 'Note: Workspace package version warnings are expected for internal @tm/* packages'", "deps:fix": "manypkg fix" }, "keywords": [ "claude", "task", "management", "ai", "development", "cursor", "anthropic", "llm", "mcp", "context" ], "author": "Eyal Toledano", "license": "MIT WITH Commons-Clause", "dependencies": { "@ai-sdk/amazon-bedrock": "^2.2.9", "@ai-sdk/anthropic": "^1.2.10", "@ai-sdk/azure": "^1.3.17", "@ai-sdk/google": "^1.2.13", "@ai-sdk/google-vertex": "^2.2.23", "@ai-sdk/groq": "^1.2.9", "@ai-sdk/mistral": "^1.2.7", "@ai-sdk/openai": "^1.3.20", "@ai-sdk/perplexity": "^1.1.7", "@ai-sdk/xai": "^1.2.15", "@anthropic-ai/sdk": "^0.39.0", "@aws-sdk/credential-providers": "^3.817.0", "@inquirer/search": "^3.0.15", "@openrouter/ai-sdk-provider": "^0.4.5", "@streamparser/json": "^0.0.22", "@supabase/supabase-js": "^2.57.4", "ai": "^4.3.10", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "boxen": "^8.0.1", "chalk": "5.6.2", "cli-highlight": "^2.1.11", "cli-progress": "^3.12.0", "cli-table3": "^0.6.5", "commander": "^12.1.0", "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.21.2", "fastmcp": "^3.5.0", "figlet": "^1.8.0", "fuse.js": "^7.1.0", "gpt-tokens": "^1.3.14", "gradient-string": "^3.0.0", "helmet": "^8.1.0", "inquirer": "^12.5.0", "jsonc-parser": "^3.3.1", "jsonrepair": "^3.13.0", "jsonwebtoken": "^9.0.2", "lru-cache": "^10.2.0", "marked": "^15.0.12", "marked-terminal": "^7.3.0", "ollama-ai-provider": "^1.2.0", "openai": "^4.89.0", "ora": "^8.2.0", "uuid": "^11.1.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.5" }, "optionalDependencies": { "@anthropic-ai/claude-code": "^1.0.88", "@biomejs/cli-linux-x64": "^1.9.4", "ai-sdk-provider-gemini-cli": "^0.1.3" }, "engines": { "node": ">=18.0.0" }, "packageManager": "[email protected]", "repository": { "type": "git", "url": "git+https://github.com/eyaltoledano/claude-task-master.git" }, "homepage": "https://github.com/eyaltoledano/claude-task-master#readme", "bugs": { "url": "https://github.com/eyaltoledano/claude-task-master/issues" }, "files": ["dist/**", "README-task-master.md", "README.md", "LICENSE"], "overrides": { "node-fetch": "^2.6.12", "whatwg-url": "^11.0.0" }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@changesets/changelog-github": "^0.5.1", "@changesets/cli": "^2.28.1", "@manypkg/cli": "^0.25.1", "@tm/cli": "*", "@types/jest": "^29.5.14", "@types/marked-terminal": "^6.1.1", "concurrently": "^9.2.1", "cross-env": "^10.0.0", "dotenv-mono": "^1.5.1", "execa": "^8.0.1", "jest": "^29.7.0", "jest-environment-node": "^29.7.0", "mock-fs": "^5.5.0", "prettier": "^3.5.3", "supertest": "^7.1.0", "ts-jest": "^29.4.2", "tsdown": "^0.15.2", "tsx": "^4.20.4", "turbo": "^2.5.6", "typescript": "^5.7.3" } } ``` -------------------------------------------------------------------------------- /docs/examples/claude-code-usage.md: -------------------------------------------------------------------------------- ```markdown # Claude Code Provider Usage Example The Claude Code provider allows you to use Claude models through the Claude Code CLI without requiring an API key. ## Configuration To use the Claude Code provider, update your `.taskmaster/config.json`: ```json { "models": { "main": { "provider": "claude-code", "modelId": "sonnet", "maxTokens": 64000, "temperature": 0.2 }, "research": { "provider": "claude-code", "modelId": "opus", "maxTokens": 32000, "temperature": 0.1 }, "fallback": { "provider": "claude-code", "modelId": "sonnet", "maxTokens": 64000, "temperature": 0.2 } } } ``` ## Available Models - `opus` - Claude Opus model (SWE score: 0.725) - `sonnet` - Claude Sonnet model (SWE score: 0.727) ## Usage Once configured, you can use Claude Code with all Task Master commands: ```bash # Generate tasks from a PRD task-master parse-prd --input=prd.txt # Analyze project complexity task-master analyze-complexity # Show the next task to work on task-master next # View a specific task task-master show task-001 # Update task status task-master set-status --id=task-001 --status=in-progress ``` ## Requirements 1. Claude Code CLI must be installed and authenticated on your system 2. Install the optional `@anthropic-ai/claude-code` package if you enable this provider: ```bash npm install @anthropic-ai/claude-code ``` 3. Run Claude Code for the first time and authenticate with your Anthropic account: ```bash claude ``` 4. No API key is required in your environment variables or MCP configuration ## Advanced Settings The Claude Code SDK supports additional settings that provide fine-grained control over Claude's behavior. These settings are implemented in the underlying SDK (`src/ai-providers/custom-sdk/claude-code/`), and can be managed through Task Master's configuration file. ### Advanced Settings Usage To update settings for Claude Code, update your `.taskmaster/config.json`: The Claude Code settings can be specified globally in the `claudeCode` section of the config, or on a per-command basis in the `commandSpecific` section: ```javascript { // "models" and "global" config... "claudeCode": { // Maximum conversation turns Claude can make in a single request "maxTurns": 5, // Custom system prompt to override Claude Code's default behavior "customSystemPrompt": "You are a helpful assistant focused on code quality", // Append additional content to the system prompt "appendSystemPrompt": "Always follow coding best practices", // Permission mode for file system operations "permissionMode": "default", // Options: "default", "acceptEdits", "plan", "bypassPermissions" // Explicitly allow only certain tools "allowedTools": ["Read", "LS"], // Claude can only read files and list directories // Explicitly disallow certain tools "disallowedTools": ["Write", "Edit"], // Prevent Claude from modifying files // MCP servers for additional tool integrations "mcpServers": { "mcp-server-name": { "command": "npx", "args": ["-y", "mcp-serve"], "env": { // ... } } } }, // Command-specific settings override global settings "commandSpecific": { "parse-prd": { // Settings specific to the 'parse-prd' command "maxTurns": 10, "customSystemPrompt": "You are a task breakdown specialist" }, "analyze-complexity": { // Settings specific to the 'analyze-complexity' command "maxTurns": 3, "appendSystemPrompt": "Focus on identifying bottlenecks" } } } ``` - For a full list of Cluaude Code settings, see the [Claude Code Settings documentation](https://docs.anthropic.com/en/docs/claude-code/settings). - For a full list of AI powered command names, see this file: `src/constants/commands.js` ### Why These Settings Matter - **maxTurns**: Useful for complex refactoring tasks that require multiple iterations - **customSystemPrompt**: Allows specializing Claude for specific domains or coding standards - **appendSystemPrompt**: Useful for enforcing coding standards or providing additional context - **permissionMode**: Critical for security in production environments - **allowedTools/disallowedTools**: Enable read-only analysis modes or restrict access to sensitive operations - **mcpServers**: Future extensibility for custom tool integrations ## Notes - The Claude Code provider doesn't track usage costs (shown as 0 in telemetry) - Session management is handled automatically for conversation continuity - Some AI SDK parameters (temperature, maxTokens) are not supported by Claude Code CLI and will be ignored ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/next-task.js: -------------------------------------------------------------------------------- ```javascript /** * next-task.js * Direct function implementation for finding the next task to work on */ import { findNextTask } from '../../../../scripts/modules/task-manager.js'; import { readJSON, readComplexityReport } from '../../../../scripts/modules/utils.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; /** * Direct function wrapper for finding the next task to work on with error handling and caching. * * @param {Object} args - Command arguments * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. * @param {string} args.reportPath - Path to the report file. * @param {string} args.projectRoot - Project root path (for MCP/env fallback) * @param {string} args.tag - Tag for the task (optional) * @param {Object} log - Logger object * @returns {Promise<Object>} - Next task result { success: boolean, data?: any, error?: { code: string, message: string } } */ export async function nextTaskDirect(args, log, context = {}) { // Destructure expected args const { tasksJsonPath, reportPath, projectRoot, tag } = args; const { session } = context; if (!tasksJsonPath) { log.error('nextTaskDirect called without tasksJsonPath'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Define the action function to be executed on cache miss const coreNextTaskAction = async () => { try { // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); log.info(`Finding next task from ${tasksJsonPath}`); // Read tasks data using the provided path const data = readJSON(tasksJsonPath, projectRoot, tag); if (!data || !data.tasks) { disableSilentMode(); // Disable before return return { success: false, error: { code: 'INVALID_TASKS_FILE', message: `No valid tasks found in ${tasksJsonPath}` } }; } // Read the complexity report const complexityReport = readComplexityReport(reportPath); // Find the next task const nextTask = findNextTask(data.tasks, complexityReport); if (!nextTask) { log.info( 'No eligible next task found. All tasks are either completed or have unsatisfied dependencies' ); return { success: true, data: { message: 'No eligible next task found. All tasks are either completed or have unsatisfied dependencies', nextTask: null } }; } // Check if it's a subtask const isSubtask = typeof nextTask.id === 'string' && nextTask.id.includes('.'); const taskOrSubtask = isSubtask ? 'subtask' : 'task'; const additionalAdvice = isSubtask ? 'Subtasks can be updated with timestamped details as you implement them. This is useful for tracking progress, marking milestones and insights (of successful or successive falures in attempting to implement the subtask). Research can be used when updating the subtask to collect up-to-date information, and can be helpful to solve a repeating problem the agent is unable to solve. It is a good idea to get-task the parent task to collect the overall context of the task, and to get-task the subtask to collect the specific details of the subtask.' : 'Tasks can be updated to reflect a change in the direction of the task, or to reformulate the task per your prompt. Research can be used when updating the task to collect up-to-date information. It is best to update subtasks as you work on them, and to update the task for more high-level changes that may affect pending subtasks or the general direction of the task.'; // Restore normal logging disableSilentMode(); // Return the next task data with the full tasks array for reference log.info( `Successfully found next task ${nextTask.id}: ${nextTask.title}. Is subtask: ${isSubtask}` ); return { success: true, data: { nextTask, isSubtask, nextSteps: `When ready to work on the ${taskOrSubtask}, use set-status to set the status to "in progress" ${additionalAdvice}` } }; } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error finding next task: ${error.message}`); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: error.message || 'Failed to find next task' } }; } }; // Use the caching utility try { const result = await coreNextTaskAction(); log.info('nextTaskDirect completed.'); return result; } catch (error) { log.error(`Unexpected error during nextTask: ${error.message}`); return { success: false, error: { code: 'UNEXPECTED_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /.github/workflows/claude-issue-triage.yml: -------------------------------------------------------------------------------- ```yaml name: Claude Issue Triage # description: Automatically triage GitHub issues using Claude Code on: issues: types: [opened] jobs: triage-issue: runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read issues: write steps: - name: Checkout repository uses: actions/checkout@v4 - name: Create triage prompt run: | mkdir -p /tmp/claude-prompts cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF' You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list. IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels. Issue Information: - REPO: ${{ github.repository }} - ISSUE_NUMBER: ${{ github.event.issue.number }} TASK OVERVIEW: 1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else. 2. Next, use the GitHub tools to get context about the issue: - You have access to these tools: - mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels - mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments - mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting) - mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues - mcp__github__list_issues: Use this to understand patterns in how other issues are labeled - Start by using mcp__github__get_issue to get the issue details 3. Analyze the issue content, considering: - The issue title and description - The type of issue (bug report, feature request, question, etc.) - Technical areas mentioned - Severity or priority indicators - User impact - Components affected 4. Select appropriate labels from the available labels list provided above: - Choose labels that accurately reflect the issue's nature - Be specific but comprehensive - Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority) - Consider platform labels (android, ios) if applicable - If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue. 5. Apply the selected labels: - Use mcp__github__update_issue to apply your selected labels - DO NOT post any comments explaining your decision - DO NOT communicate directly with users - If no labels are clearly applicable, do not apply any labels IMPORTANT GUIDELINES: - Be thorough in your analysis - Only select labels from the provided list above - DO NOT post any comments to the issue - Your ONLY action should be to apply labels using mcp__github__update_issue - It's okay to not add any labels if none are clearly applicable EOF - name: Setup GitHub MCP Server run: | mkdir -p /tmp/mcp-config cat > /tmp/mcp-config/mcp-servers.json << 'EOF' { "mcpServers": { "github": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server:sha-7aced2b" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" } } } } EOF - name: Run Claude Code for Issue Triage uses: anthropics/claude-code-base-action@beta with: prompt_file: /tmp/claude-prompts/triage-prompt.txt allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues" timeout_minutes: "5" anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} mcp_config: /tmp/mcp-config/mcp-servers.json claude_env: | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/analyze.js: -------------------------------------------------------------------------------- ```javascript /** * tools/analyze.js * Tool for analyzing task complexity and generating recommendations */ import { z } from 'zod'; import path from 'path'; import fs from 'fs'; // Import fs for directory check/creation import { handleApiResult, createErrorResponse, withNormalizedProjectRoot } from './utils.js'; import { analyzeTaskComplexityDirect } from '../core/task-master-core.js'; // Assuming core functions are exported via task-master-core.js import { findTasksPath } from '../core/utils/path-utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js'; import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js'; import { resolveComplexityReportOutputPath } from '../../../src/utils/path-utils.js'; /** * Register the analyze_project_complexity tool * @param {Object} server - FastMCP server instance */ export function registerAnalyzeProjectComplexityTool(server) { server.addTool({ name: 'analyze_project_complexity', description: 'Analyze task complexity and generate expansion recommendations.', parameters: z.object({ threshold: z.coerce // Use coerce for number conversion from string if needed .number() .int() .min(1) .max(10) .optional() .default(5) // Default threshold .describe('Complexity score threshold (1-10) to recommend expansion.'), research: z .boolean() .optional() .default(false) .describe('Use Perplexity AI for research-backed analysis.'), output: z .string() .optional() .describe( `Output file path relative to project root (default: ${COMPLEXITY_REPORT_FILE}).` ), file: z .string() .optional() .describe( 'Path to the tasks file relative to project root (default: tasks/tasks.json).' ), ids: z .string() .optional() .describe( 'Comma-separated list of task IDs to analyze specifically (e.g., "1,3,5").' ), from: z.coerce .number() .int() .positive() .optional() .describe('Starting task ID in a range to analyze.'), to: z.coerce .number() .int() .positive() .optional() .describe('Ending task ID in a range to analyze.'), projectRoot: z .string() .describe('The directory of the project. Must be an absolute path.'), tag: z.string().optional().describe('Tag context to operate on') }), execute: withNormalizedProjectRoot(async (args, { log, session }) => { const toolName = 'analyze_project_complexity'; // Define tool name for logging try { log.info( `Executing ${toolName} tool with args: ${JSON.stringify(args)}` ); const resolvedTag = resolveTag({ projectRoot: args.projectRoot, tag: args.tag }); let tasksJsonPath; try { tasksJsonPath = findTasksPath( { projectRoot: args.projectRoot, file: args.file }, log ); log.info(`${toolName}: Resolved tasks path: ${tasksJsonPath}`); } catch (error) { log.error(`${toolName}: Error finding tasks.json: ${error.message}`); return createErrorResponse( `Failed to find tasks.json within project root '${args.projectRoot}': ${error.message}` ); } const outputPath = resolveComplexityReportOutputPath( args.output, { projectRoot: args.projectRoot, tag: resolvedTag }, log ); log.info(`${toolName}: Report output path: ${outputPath}`); // Ensure output directory exists const outputDir = path.dirname(outputPath); try { if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); log.info(`${toolName}: Created output directory: ${outputDir}`); } } catch (dirError) { log.error( `${toolName}: Failed to create output directory ${outputDir}: ${dirError.message}` ); return createErrorResponse( `Failed to create output directory: ${dirError.message}` ); } // 3. Call Direct Function - Pass projectRoot in first arg object const result = await analyzeTaskComplexityDirect( { tasksJsonPath: tasksJsonPath, outputPath: outputPath, threshold: args.threshold, research: args.research, projectRoot: args.projectRoot, tag: resolvedTag, ids: args.ids, from: args.from, to: args.to }, log, { session } ); // 4. Handle Result log.info( `${toolName}: Direct function result: success=${result.success}` ); return handleApiResult( result, log, 'Error analyzing task complexity', undefined, args.projectRoot ); } catch (error) { log.error( `Critical error in ${toolName} tool execute: ${error.message}` ); return createErrorResponse( `Internal tool error (${toolName}): ${error.message}` ); } }) }); } ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/update-subtask-by-id.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Provide fs mock early so existsSync can be stubbed jest.unstable_mockModule('fs', () => { const mockFs = { existsSync: jest.fn(() => true), writeFileSync: jest.fn(), readFileSync: jest.fn(), unlinkSync: jest.fn() }; return { default: mockFs, ...mockFs }; }); // --- Mock dependencies --- jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ readJSON: jest.fn(), writeJSON: jest.fn(), log: jest.fn(), isSilentMode: jest.fn(() => false), findProjectRoot: jest.fn(() => '/project'), flattenTasksWithSubtasks: jest.fn(() => []), truncate: jest.fn((t) => t), isEmpty: jest.fn(() => false), resolveEnvVariable: jest.fn(), findTaskById: jest.fn(), getCurrentTag: jest.fn(() => 'master') })); jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({ getStatusWithColor: jest.fn((s) => s), startLoadingIndicator: jest.fn(() => ({ stop: jest.fn() })), stopLoadingIndicator: jest.fn(), displayAiUsageSummary: jest.fn() })); jest.unstable_mockModule( '../../../../../scripts/modules/task-manager/generate-task-files.js', () => ({ default: jest.fn().mockResolvedValue() }) ); jest.unstable_mockModule( '../../../../../scripts/modules/ai-services-unified.js', () => ({ generateTextService: jest .fn() .mockResolvedValue({ mainResult: { content: '' }, telemetryData: {} }) }) ); jest.unstable_mockModule( '../../../../../scripts/modules/config-manager.js', () => ({ getDebugFlag: jest.fn(() => false), hasCodebaseAnalysis: jest.fn(() => false) }) ); jest.unstable_mockModule( '../../../../../scripts/modules/prompt-manager.js', () => ({ default: jest.fn().mockReturnValue({ loadPrompt: jest.fn().mockReturnValue('Update the subtask') }), getPromptManager: jest.fn().mockReturnValue({ loadPrompt: jest.fn().mockReturnValue('Update the subtask') }) }) ); jest.unstable_mockModule( '../../../../../scripts/modules/utils/contextGatherer.js', () => ({ ContextGatherer: jest.fn().mockImplementation(() => ({ gather: jest.fn().mockReturnValue({ fullContext: '', summary: '' }) })) }) ); // Import mocked utils to leverage mocks later const { readJSON, log } = await import( '../../../../../scripts/modules/utils.js' ); // Import function under test const { default: updateSubtaskById } = await import( '../../../../../scripts/modules/task-manager/update-subtask-by-id.js' ); describe('updateSubtaskById validation', () => { beforeEach(() => { jest.clearAllMocks(); jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); }); test('throws error on invalid subtask id format', async () => { await expect( updateSubtaskById( 'tasks/tasks.json', 'invalid', 'my prompt', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Invalid subtask ID format'); }); test('throws error when prompt is empty', async () => { await expect( updateSubtaskById( 'tasks/tasks.json', '1.1', '', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Prompt cannot be empty'); }); test('throws error if tasks file does not exist', async () => { // Mock fs.existsSync to return false via jest.spyOn (dynamic import of fs) const fs = await import('fs'); fs.existsSync.mockReturnValue(false); await expect( updateSubtaskById( 'tasks/tasks.json', '1.1', 'prompt', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Tasks file not found'); }); test('throws error if parent task missing', async () => { // Mock existsSync true const fs = await import('fs'); fs.existsSync.mockReturnValue(true); // readJSON returns tasks without parent id 1 readJSON.mockReturnValue({ tag: 'master', tasks: [] }); await expect( updateSubtaskById( 'tasks/tasks.json', '1.1', 'prompt', false, { tag: 'master' }, 'json' ) ).rejects.toThrow('Parent task with ID 1 not found'); // log called with error level expect(log).toHaveBeenCalled(); }); test('successfully updates subtask with valid inputs', async () => { const fs = await import('fs'); const { writeJSON } = await import( '../../../../../scripts/modules/utils.js' ); fs.existsSync.mockReturnValue(true); readJSON.mockReturnValue({ tag: 'master', tasks: [ { id: 1, title: 'Parent Task', subtasks: [{ id: 1, title: 'Original subtask', status: 'pending' }] } ] }); // updateSubtaskById doesn't return a value on success, it just executes await expect( updateSubtaskById( 'tasks/tasks.json', '1.1', 'Update this subtask', false, { tag: 'master' }, 'json' ) ).resolves.not.toThrow(); expect(writeJSON).toHaveBeenCalled(); }); }); ``` -------------------------------------------------------------------------------- /llms-install.md: -------------------------------------------------------------------------------- ```markdown # Taskmaster AI Installation Guide This guide helps AI assistants install and configure Taskmaster for users in their development projects. ## What is Taskmaster? Taskmaster is an AI-driven task management system designed for development workflows. It helps break down projects into manageable tasks, track dependencies, and maintain development momentum through structured, AI-enhanced planning. ## Installation Steps ### Step 1: Add MCP Configuration Add the following configuration to the user's MCP settings file (`.cursor/mcp.json` for Cursor, or equivalent for other editors): ```json { "mcpServers": { "taskmaster-ai": { "command": "npx", "args": ["-y", "task-master-ai"], "env": { "ANTHROPIC_API_KEY": "user_will_add_their_key_here", "PERPLEXITY_API_KEY": "user_will_add_their_key_here", "OPENAI_API_KEY": "user_will_add_their_key_here", "GOOGLE_API_KEY": "user_will_add_their_key_here", "MISTRAL_API_KEY": "user_will_add_their_key_here", "OPENROUTER_API_KEY": "user_will_add_their_key_here", "XAI_API_KEY": "user_will_add_their_key_here" } } } } ``` ### Step 2: API Key Requirements Inform the user they need **at least one** API key from the following providers: - **Anthropic** (for Claude models) - Recommended - **OpenAI** (for GPT models) - **Google** (for Gemini models) - **Perplexity** (for research features) - Highly recommended - **Mistral** (for Mistral models) - **OpenRouter** (access to multiple models) - **xAI** (for Grok models) The user will be able to define 3 separate roles (can be the same provider or separate providers) for main AI operations, research operations (research providers/models only), and a fallback model in case of errors. ### Step 3: Initialize Project Once the MCP server is configured and API keys are added, initialize Taskmaster in the user's project: > Can you initialize Task Master in my project? This will run the `initialize_project` tool to set up the basic file structure. ### Step 4: Create Initial Tasks Users have two options for creating initial tasks: **Option A: Parse a PRD (Recommended)** If they have a Product Requirements Document: > Can you parse my PRD file at [path/to/prd.txt] to generate initial tasks? If the user does not have a PRD, the AI agent can help them create one and store it in scripts/prd.txt for parsing. **Option B: Start from scratch** > Can you help me add my first task: [describe the task] ## Common Usage Patterns ### Daily Workflow > What's the next task I should work on? > Can you show me the details for task [ID]? > Can you mark task [ID] as done? ### Task Management > Can you break down task [ID] into subtasks? > Can you add a new task: [description] > Can you analyze the complexity of my tasks? ### Project Organization > Can you show me all my pending tasks? > Can you move task [ID] to become a subtask of [parent ID]? > Can you update task [ID] with this new information: [details] ## Verification Steps After installation, verify everything is working: 1. **Check MCP Connection**: The AI should be able to access Task Master tools 2. **Test Basic Commands**: Try `get_tasks` to list current tasks 3. **Verify API Keys**: Ensure AI-powered commands work (like `add_task`) Note: An API key fallback exists that allows the MCP server to read API keys from `.env` instead of the MCP JSON config. It is recommended to have keys in both places in case the MCP server is unable to read keys from its environment for whatever reason. When adding keys to `.env` only, the `models` tool will explain that the keys are not OK for MCP. Despite this, the fallback should kick in and the API keys will be read from the `.env` file. ## Troubleshooting **If MCP server doesn't start:** - Verify the JSON configuration is valid - Check that Node.js is installed - Ensure API keys are properly formatted **If AI commands fail:** - Verify at least one API key is configured - Check API key permissions and quotas - Try using a different model via the `models` tool ## CLI Fallback Taskmaster is also available via CLI commands, by installing with `npm install task-master-ai@latest` in a terminal. Running `task-master help` will show all available commands, which offer a 1:1 experience with the MCP server. As the AI agent, you should refer to the system prompts and rules provided to you to identify Taskmaster-specific rules that help you understand how and when to use it. ## Next Steps Once installed, users can: - Create new tasks with `add-task` or parse a PRD (scripts/prd.txt) into tasks with `parse-prd` - Set up model preferences with `models` tool - Expand tasks into subtasks with `expand-all` and `expand-task` - Explore advanced features like research mode and complexity analysis For detailed documentation, refer to the Task Master docs directory.`` ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/add-subtask.js: -------------------------------------------------------------------------------- ```javascript import path from 'path'; import { log, readJSON, writeJSON, getCurrentTag } from '../utils.js'; import { isTaskDependentOn } from '../task-manager.js'; import generateTaskFiles from './generate-task-files.js'; /** * Add a subtask to a parent task * @param {string} tasksPath - Path to the tasks.json file * @param {number|string} parentId - ID of the parent task * @param {number|string|null} existingTaskId - ID of an existing task to convert to subtask (optional) * @param {Object} newSubtaskData - Data for creating a new subtask (used if existingTaskId is null) * @param {boolean} generateFiles - Whether to regenerate task files after adding the subtask * @param {Object} context - Context object containing projectRoot and tag information * @param {string} context.projectRoot - Project root path * @param {string} context.tag - Tag for the task * @returns {Object} The newly created or converted subtask */ async function addSubtask( tasksPath, parentId, existingTaskId = null, newSubtaskData = null, generateFiles = false, context = {} ) { const { projectRoot, tag } = context; try { log('info', `Adding subtask to parent task ${parentId}...`); // Read the existing tasks with proper context const data = readJSON(tasksPath, projectRoot, tag); if (!data || !data.tasks) { throw new Error(`Invalid or missing tasks file at ${tasksPath}`); } // Convert parent ID to number const parentIdNum = parseInt(parentId, 10); // Find the parent task const parentTask = data.tasks.find((t) => t.id === parentIdNum); if (!parentTask) { throw new Error(`Parent task with ID ${parentIdNum} not found`); } // Initialize subtasks array if it doesn't exist if (!parentTask.subtasks) { parentTask.subtasks = []; } let newSubtask; // Case 1: Convert an existing task to a subtask if (existingTaskId !== null) { const existingTaskIdNum = parseInt(existingTaskId, 10); // Find the existing task const existingTaskIndex = data.tasks.findIndex( (t) => t.id === existingTaskIdNum ); if (existingTaskIndex === -1) { throw new Error(`Task with ID ${existingTaskIdNum} not found`); } const existingTask = data.tasks[existingTaskIndex]; // Check if task is already a subtask if (existingTask.parentTaskId) { throw new Error( `Task ${existingTaskIdNum} is already a subtask of task ${existingTask.parentTaskId}` ); } // Check for circular dependency if (existingTaskIdNum === parentIdNum) { throw new Error(`Cannot make a task a subtask of itself`); } // Check if parent task is a subtask of the task we're converting // This would create a circular dependency if (isTaskDependentOn(data.tasks, parentTask, existingTaskIdNum)) { throw new Error( `Cannot create circular dependency: task ${parentIdNum} is already a subtask or dependent of task ${existingTaskIdNum}` ); } // Find the highest subtask ID to determine the next ID const highestSubtaskId = parentTask.subtasks.length > 0 ? Math.max(...parentTask.subtasks.map((st) => st.id)) : 0; const newSubtaskId = highestSubtaskId + 1; // Clone the existing task to be converted to a subtask newSubtask = { ...existingTask, id: newSubtaskId, parentTaskId: parentIdNum }; // Add to parent's subtasks parentTask.subtasks.push(newSubtask); // Remove the task from the main tasks array data.tasks.splice(existingTaskIndex, 1); log( 'info', `Converted task ${existingTaskIdNum} to subtask ${parentIdNum}.${newSubtaskId}` ); } // Case 2: Create a new subtask else if (newSubtaskData) { // Find the highest subtask ID to determine the next ID const highestSubtaskId = parentTask.subtasks.length > 0 ? Math.max(...parentTask.subtasks.map((st) => st.id)) : 0; const newSubtaskId = highestSubtaskId + 1; // Create the new subtask object newSubtask = { id: newSubtaskId, title: newSubtaskData.title, description: newSubtaskData.description || '', details: newSubtaskData.details || '', status: newSubtaskData.status || 'pending', dependencies: newSubtaskData.dependencies || [], parentTaskId: parentIdNum }; // Add to parent's subtasks parentTask.subtasks.push(newSubtask); log('info', `Created new subtask ${parentIdNum}.${newSubtaskId}`); } else { throw new Error( 'Either existingTaskId or newSubtaskData must be provided' ); } // Write the updated tasks back to the file with proper context writeJSON(tasksPath, data, projectRoot, tag); // Generate task files if requested if (generateFiles) { log('info', 'Regenerating task files...'); await generateTaskFiles(tasksPath, path.dirname(tasksPath), context); } return newSubtask; } catch (error) { log('error', `Error adding subtask: ${error.message}`); throw error; } } export default addSubtask; ``` -------------------------------------------------------------------------------- /.github/scripts/backfill-duplicate-comments.mjs: -------------------------------------------------------------------------------- ``` #!/usr/bin/env node async function githubRequest(endpoint, token, method = 'GET', body) { const response = await fetch(`https://api.github.com${endpoint}`, { method, headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'backfill-duplicate-comments-script', ...(body && { 'Content-Type': 'application/json' }) }, ...(body && { body: JSON.stringify(body) }) }); if (!response.ok) { throw new Error( `GitHub API request failed: ${response.status} ${response.statusText}` ); } return response.json(); } async function triggerDedupeWorkflow( owner, repo, issueNumber, token, dryRun = true ) { if (dryRun) { console.log( `[DRY RUN] Would trigger dedupe workflow for issue #${issueNumber}` ); return; } await githubRequest( `/repos/${owner}/${repo}/actions/workflows/claude-dedupe-issues.yml/dispatches`, token, 'POST', { ref: 'main', inputs: { issue_number: issueNumber.toString() } } ); } async function backfillDuplicateComments() { console.log('[DEBUG] Starting backfill duplicate comments script'); const token = process.env.GITHUB_TOKEN; if (!token) { throw new Error(`GITHUB_TOKEN environment variable is required Usage: node .github/scripts/backfill-duplicate-comments.mjs Environment Variables: GITHUB_TOKEN - GitHub personal access token with repo and actions permissions (required) DRY_RUN - Set to "false" to actually trigger workflows (default: true for safety) DAYS_BACK - How many days back to look for old issues (default: 90)`); } console.log('[DEBUG] GitHub token found'); const owner = process.env.GITHUB_REPOSITORY_OWNER || 'eyaltoledano'; const repo = process.env.GITHUB_REPOSITORY_NAME || 'claude-task-master'; const dryRun = process.env.DRY_RUN !== 'false'; const daysBack = parseInt(process.env.DAYS_BACK || '90', 10); console.log(`[DEBUG] Repository: ${owner}/${repo}`); console.log(`[DEBUG] Dry run mode: ${dryRun}`); console.log(`[DEBUG] Looking back ${daysBack} days`); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - daysBack); console.log( `[DEBUG] Fetching issues created since ${cutoffDate.toISOString()}...` ); const allIssues = []; let page = 1; const perPage = 100; while (true) { const pageIssues = await githubRequest( `/repos/${owner}/${repo}/issues?state=all&per_page=${perPage}&page=${page}&since=${cutoffDate.toISOString()}`, token ); if (pageIssues.length === 0) break; allIssues.push(...pageIssues); page++; // Safety limit to avoid infinite loops if (page > 100) { console.log('[DEBUG] Reached page limit, stopping pagination'); break; } } console.log( `[DEBUG] Found ${allIssues.length} issues from the last ${daysBack} days` ); let processedCount = 0; let candidateCount = 0; let triggeredCount = 0; for (const issue of allIssues) { processedCount++; console.log( `[DEBUG] Processing issue #${issue.number} (${processedCount}/${allIssues.length}): ${issue.title}` ); console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`); const comments = await githubRequest( `/repos/${owner}/${repo}/issues/${issue.number}/comments`, token ); console.log( `[DEBUG] Issue #${issue.number} has ${comments.length} comments` ); // Look for existing duplicate detection comments (from the dedupe bot) const dupeDetectionComments = comments.filter( (comment) => comment.body.includes('Found') && comment.body.includes('possible duplicate') && comment.user.type === 'Bot' ); console.log( `[DEBUG] Issue #${issue.number} has ${dupeDetectionComments.length} duplicate detection comments` ); // Skip if there's already a duplicate detection comment if (dupeDetectionComments.length > 0) { console.log( `[DEBUG] Issue #${issue.number} already has duplicate detection comment, skipping` ); continue; } candidateCount++; const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`; try { console.log( `[INFO] ${dryRun ? '[DRY RUN] ' : ''}Triggering dedupe workflow for issue #${issue.number}: ${issueUrl}` ); await triggerDedupeWorkflow(owner, repo, issue.number, token, dryRun); if (!dryRun) { console.log( `[SUCCESS] Successfully triggered dedupe workflow for issue #${issue.number}` ); } triggeredCount++; } catch (error) { console.error( `[ERROR] Failed to trigger workflow for issue #${issue.number}: ${error}` ); } // Add a delay between workflow triggers to avoid overwhelming the system await new Promise((resolve) => setTimeout(resolve, 1000)); } console.log( `[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates without duplicate comments, ${dryRun ? 'would trigger' : 'triggered'} ${triggeredCount} workflows` ); } backfillDuplicateComments().catch(console.error); ``` -------------------------------------------------------------------------------- /apps/docs/archive/ai-development-workflow.mdx: -------------------------------------------------------------------------------- ```markdown --- title: "AI Development Workflow" description: "Learn how Task Master and Cursor AI work together to streamline your development workflow" --- <Tip>The Cursor agent is pre-configured (via the rules file) to follow this workflow</Tip> <AccordionGroup> <Accordion title="1. Task Discovery and Selection"> Ask the agent to list available tasks: ``` What tasks are available to work on next? ``` The agent will: - Run `task-master list` to see all tasks - Run `task-master next` to determine the next task to work on - Analyze dependencies to determine which tasks are ready to be worked on - Prioritize tasks based on priority level and ID order - Suggest the next task(s) to implement </Accordion> <Accordion title="2. Task Implementation"> When implementing a task, the agent will: - Reference the task's details section for implementation specifics - Consider dependencies on previous tasks - Follow the project's coding standards - Create appropriate tests based on the task's testStrategy You can ask: ``` Let's implement task 3. What does it involve? ``` </Accordion> <Accordion title="3. Task Verification"> Before marking a task as complete, verify it according to: - The task's specified testStrategy - Any automated tests in the codebase - Manual verification if required </Accordion> <Accordion title="4. Task Completion"> When a task is completed, tell the agent: ``` Task 3 is now complete. Please update its status. ``` The agent will execute: ```bash task-master set-status --id=3 --status=done ``` </Accordion> <Accordion title="5. Handling Implementation Drift"> If during implementation, you discover that: - The current approach differs significantly from what was planned - Future tasks need to be modified due to current implementation choices - New dependencies or requirements have emerged Tell the agent: ``` We've changed our approach. We're now using Express instead of Fastify. Please update all future tasks to reflect this change. ``` The agent will execute: ```bash task-master update --from=4 --prompt="Now we are using Express instead of Fastify." ``` This will rewrite or re-scope subsequent tasks in tasks.json while preserving completed work. </Accordion> <Accordion title="6. Breaking Down Complex Tasks"> For complex tasks that need more granularity: ``` Task 5 seems complex. Can you break it down into subtasks? ``` The agent will execute: ```bash task-master expand --id=5 --num=3 ``` You can provide additional context: ``` Please break down task 5 with a focus on security considerations. ``` The agent will execute: ```bash task-master expand --id=5 --prompt="Focus on security aspects" ``` You can also expand all pending tasks: ``` Please break down all pending tasks into subtasks. ``` The agent will execute: ```bash task-master expand --all ``` For research-backed subtask generation using Perplexity AI: ``` Please break down task 5 using research-backed generation. ``` The agent will execute: ```bash task-master expand --id=5 --research ``` </Accordion> </AccordionGroup> ## Example Cursor AI Interactions <AccordionGroup> <Accordion title="Starting a new project"> ``` I've just initialized a new project with Claude Task Master. I have a PRD at scripts/prd.txt. Can you help me parse it and set up the initial tasks? ``` </Accordion> <Accordion title="Working on tasks"> ``` What's the next task I should work on? Please consider dependencies and priorities. ``` </Accordion> <Accordion title="Implementing a specific task"> ``` I'd like to implement task 4. Can you help me understand what needs to be done and how to approach it? ``` </Accordion> <Accordion title="Managing subtasks"> ``` I need to regenerate the subtasks for task 3 with a different approach. Can you help me clear and regenerate them? ``` </Accordion> <Accordion title="Handling changes"> ``` We've decided to use MongoDB instead of PostgreSQL. Can you update all future tasks to reflect this change? ``` </Accordion> <Accordion title="Completing work"> ``` I've finished implementing the authentication system described in task 2. All tests are passing. Please mark it as complete and tell me what I should work on next. ``` </Accordion> <Accordion title="Analyzing complexity"> ``` Can you analyze the complexity of our tasks to help me understand which ones need to be broken down further? ``` </Accordion> <Accordion title="Viewing complexity report"> ``` Can you show me the complexity report in a more readable format? ``` </Accordion> </AccordionGroup> ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/reducers/appReducer.ts: -------------------------------------------------------------------------------- ```typescript /** * Main application state reducer */ import type { AppState, AppAction } from '../types'; import { logger } from '../utils/logger'; export const appReducer = (state: AppState, action: AppAction): AppState => { logger.debug( 'Reducer action:', action.type, 'payload' in action ? action.payload : 'no payload' ); switch (action.type) { case 'SET_TASKS': const newTasks = Array.isArray(action.payload) ? action.payload : []; logger.debug('SET_TASKS reducer - updating tasks:', { oldCount: state.tasks.length, newCount: newTasks.length, newTasks }); return { ...state, tasks: newTasks, loading: false, error: undefined }; case 'SET_LOADING': return { ...state, loading: action.payload }; case 'SET_ERROR': return { ...state, error: action.payload, loading: false }; case 'CLEAR_ERROR': return { ...state, error: undefined }; case 'INCREMENT_REQUEST_ID': return { ...state, requestId: state.requestId + 1 }; case 'UPDATE_TASK_STATUS': { const { taskId, newStatus } = action.payload; return { ...state, tasks: state.tasks.map((task) => task.id === taskId ? { ...task, status: newStatus } : task ) }; } case 'UPDATE_TASK_CONTENT': { const { taskId, updates } = action.payload; return { ...state, tasks: state.tasks.map((task) => task.id === taskId ? { ...task, ...updates } : task ) }; } case 'SET_CONNECTION_STATUS': return { ...state, isConnected: action.payload.isConnected, connectionStatus: action.payload.status }; case 'SET_EDITING_TASK': return { ...state, editingTask: action.payload }; case 'SET_POLLING_STATUS': return { ...state, polling: { ...state.polling, isActive: action.payload.isActive, errorCount: action.payload.errorCount ?? state.polling.errorCount, lastUpdate: action.payload.isActive ? Date.now() : state.polling.lastUpdate } }; case 'SET_USER_INTERACTING': return { ...state, polling: { ...state.polling, isUserInteracting: action.payload } }; case 'TASKS_UPDATED_FROM_POLLING': return { ...state, tasks: Array.isArray(action.payload) ? action.payload : [], polling: { ...state.polling, lastUpdate: Date.now() } }; case 'SET_NETWORK_STATUS': return { ...state, polling: { ...state.polling, isOfflineMode: action.payload.isOfflineMode, connectionStatus: action.payload.connectionStatus, reconnectAttempts: action.payload.reconnectAttempts !== undefined ? action.payload.reconnectAttempts : state.polling.reconnectAttempts, maxReconnectAttempts: action.payload.maxReconnectAttempts !== undefined ? action.payload.maxReconnectAttempts : state.polling.maxReconnectAttempts, lastSuccessfulConnection: action.payload.lastSuccessfulConnection !== undefined ? action.payload.lastSuccessfulConnection : state.polling.lastSuccessfulConnection } }; case 'LOAD_CACHED_TASKS': return { ...state, tasks: Array.isArray(action.payload) ? action.payload : [] }; case 'ADD_TOAST': return { ...state, toastNotifications: [...state.toastNotifications, action.payload] }; case 'REMOVE_TOAST': return { ...state, toastNotifications: state.toastNotifications.filter( (notification) => notification.id !== action.payload ) }; case 'CLEAR_ALL_TOASTS': return { ...state, toastNotifications: [] }; case 'NAVIGATE_TO_TASK': logger.debug('📍 Reducer: Navigating to task:', action.payload); return { ...state, currentView: 'task-details', selectedTaskId: action.payload }; case 'NAVIGATE_TO_KANBAN': logger.debug('📍 Reducer: Navigating to kanban'); return { ...state, currentView: 'kanban', selectedTaskId: undefined }; case 'NAVIGATE_TO_CONFIG': logger.debug('📍 Reducer: Navigating to config'); return { ...state, currentView: 'config', selectedTaskId: undefined }; case 'SET_CURRENT_TAG': return { ...state, currentTag: action.payload }; case 'SET_AVAILABLE_TAGS': return { ...state, availableTags: action.payload }; case 'SET_TAG_DATA': return { ...state, currentTag: action.payload.currentTag, availableTags: action.payload.availableTags }; default: return state; } }; export const initialState: AppState = { tasks: [], loading: true, requestId: 0, isConnected: false, connectionStatus: 'Connecting...', editingTask: { taskId: null }, polling: { isActive: false, errorCount: 0, lastUpdate: undefined, isUserInteracting: false, isOfflineMode: false, reconnectAttempts: 0, maxReconnectAttempts: 0, lastSuccessfulConnection: undefined, connectionStatus: 'online' }, toastNotifications: [], currentView: 'kanban', selectedTaskId: undefined, // Tag-related state currentTag: 'master', availableTags: ['master'] }; ``` -------------------------------------------------------------------------------- /src/prompts/add-task.json: -------------------------------------------------------------------------------- ```json { "id": "add-task", "version": "1.0.0", "description": "Generate a new task based on description", "metadata": { "author": "system", "created": "2024-01-01T00:00:00Z", "updated": "2024-01-01T00:00:00Z", "tags": ["task-creation", "generation"] }, "parameters": { "prompt": { "type": "string", "required": true, "description": "User's task description" }, "newTaskId": { "type": "number", "required": true, "description": "ID for the new task" }, "existingTasks": { "type": "array", "description": "List of existing tasks for context" }, "gatheredContext": { "type": "string", "description": "Context gathered from codebase analysis" }, "contextFromArgs": { "type": "string", "description": "Additional context from manual args" }, "priority": { "type": "string", "default": "medium", "enum": ["high", "medium", "low"], "description": "Task priority" }, "dependencies": { "type": "array", "description": "Task dependency IDs" }, "useResearch": { "type": "boolean", "default": false, "description": "Use research mode" }, "hasCodebaseAnalysis": { "type": "boolean", "required": false, "default": false, "description": "Whether codebase analysis is available" }, "projectRoot": { "type": "string", "required": false, "default": "", "description": "Project root path for context" } }, "prompts": { "default": { "system": "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\nWhen determining dependencies for a new task, follow these principles:\n1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n2. Prioritize task dependencies that are semantically related to the functionality being built.\n3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\nThe dependencies array should contain task IDs (numbers) of prerequisite tasks.{{#if useResearch}}\n\nResearch current best practices and technologies relevant to this task.{{/if}}", "user": "{{#if hasCodebaseAnalysis}}## IMPORTANT: Codebase Analysis Required\n\nYou have access to powerful codebase analysis tools. Before generating the task:\n\n1. Use the Glob tool to explore the project structure (e.g., \"**/*.js\", \"**/*.json\", \"**/README.md\")\n2. Use the Grep tool to search for existing implementations, patterns, and technologies\n3. Use the Read tool to examine key files like package.json, main entry points, and relevant source files\n4. Analyze the current implementation to understand what already exists\n\nBased on your analysis:\n- Identify existing components/features that relate to this new task\n- Understand the technology stack, frameworks, and patterns in use\n- Generate implementation details that align with the project's current architecture\n- Reference specific files, functions, or patterns from the codebase in your details\n\nProject Root: {{projectRoot}}\n\n{{/if}}You are generating the details for Task #{{newTaskId}}. Based on the user's request: \"{{prompt}}\", create a comprehensive new task for a software development project.\n \n {{gatheredContext}}\n \n {{#if useResearch}}Research current best practices, technologies, and implementation patterns relevant to this task. {{/if}}Based on the information about existing tasks provided above, include appropriate dependencies in the \"dependencies\" array. Only include task IDs that this new task directly depends on.\n \n Return your answer as a single JSON object matching the schema precisely:\n \n {\n \"title\": \"Task title goes here\",\n \"description\": \"A concise one or two sentence description of what the task involves\",\n \"details\": \"Detailed implementation steps, considerations, code examples, or technical approach\",\n \"testStrategy\": \"Specific steps to verify correct implementation and functionality\",\n \"dependencies\": [1, 3] // Example: IDs of tasks that must be completed before this task\n }\n \n Make sure the details and test strategy are comprehensive and specific{{#if useResearch}}, incorporating current best practices from your research{{/if}}. DO NOT include the task ID in the title.\n {{#if contextFromArgs}}{{contextFromArgs}}{{/if}}" } } } ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/add-subtask.test.js: -------------------------------------------------------------------------------- ```javascript /** * Tests for the addSubtask function */ import { jest } from '@jest/globals'; // Mock dependencies before importing the module const mockUtils = { readJSON: jest.fn(), writeJSON: jest.fn(), log: jest.fn(), getCurrentTag: jest.fn() }; const mockTaskManager = { isTaskDependentOn: jest.fn() }; const mockGenerateTaskFiles = jest.fn(); jest.unstable_mockModule( '../../../../../scripts/modules/utils.js', () => mockUtils ); jest.unstable_mockModule( '../../../../../scripts/modules/task-manager.js', () => mockTaskManager ); jest.unstable_mockModule( '../../../../../scripts/modules/task-manager/generate-task-files.js', () => ({ default: mockGenerateTaskFiles }) ); const addSubtask = ( await import('../../../../../scripts/modules/task-manager/add-subtask.js') ).default; describe('addSubtask function', () => { const multiTagData = { master: { tasks: [{ id: 1, title: 'Master Task', subtasks: [] }], metadata: { description: 'Master tasks' } }, 'feature-branch': { tasks: [{ id: 1, title: 'Feature Task', subtasks: [] }], metadata: { description: 'Feature tasks' } } }; beforeEach(() => { jest.clearAllMocks(); mockTaskManager.isTaskDependentOn.mockReturnValue(false); }); test('should add a new subtask and preserve other tags', async () => { const context = { projectRoot: '/fake/root', tag: 'feature-branch' }; const newSubtaskData = { title: 'My New Subtask' }; mockUtils.readJSON.mockReturnValueOnce({ tasks: [{ id: 1, title: 'Feature Task', subtasks: [] }], metadata: { description: 'Feature tasks' } }); await addSubtask('tasks.json', '1', null, newSubtaskData, true, context); expect(mockUtils.writeJSON).toHaveBeenCalledWith( 'tasks.json', expect.any(Object), '/fake/root', 'feature-branch' ); const writtenData = mockUtils.writeJSON.mock.calls[0][1]; const parentTask = writtenData.tasks.find((t) => t.id === 1); expect(parentTask.subtasks).toHaveLength(1); expect(parentTask.subtasks[0].title).toBe('My New Subtask'); }); test('should add a new subtask to a parent task', async () => { mockUtils.readJSON.mockReturnValueOnce({ tasks: [{ id: 1, title: 'Parent Task', subtasks: [] }] }); const context = {}; const newSubtask = await addSubtask( 'tasks.json', '1', null, { title: 'New Subtask' }, true, context ); expect(newSubtask).toBeDefined(); expect(newSubtask.id).toBe(1); expect(newSubtask.parentTaskId).toBe(1); expect(mockUtils.writeJSON).toHaveBeenCalled(); const writeCallArgs = mockUtils.writeJSON.mock.calls[0][1]; // data is the second arg now const parentTask = writeCallArgs.tasks.find((t) => t.id === 1); expect(parentTask.subtasks).toHaveLength(1); expect(parentTask.subtasks[0].title).toBe('New Subtask'); expect(mockGenerateTaskFiles).toHaveBeenCalled(); }); test('should convert an existing task to a subtask', async () => { mockUtils.readJSON.mockReturnValueOnce({ tasks: [ { id: 1, title: 'Parent Task', subtasks: [] }, { id: 2, title: 'Existing Task 2', subtasks: [] } ] }); const context = {}; const convertedSubtask = await addSubtask( 'tasks.json', '1', '2', null, true, context ); expect(convertedSubtask.id).toBe(1); expect(convertedSubtask.parentTaskId).toBe(1); expect(convertedSubtask.title).toBe('Existing Task 2'); expect(mockUtils.writeJSON).toHaveBeenCalled(); const writeCallArgs = mockUtils.writeJSON.mock.calls[0][1]; const parentTask = writeCallArgs.tasks.find((t) => t.id === 1); expect(parentTask.subtasks).toHaveLength(1); expect(parentTask.subtasks[0].title).toBe('Existing Task 2'); }); test('should throw an error if parent task does not exist', async () => { mockUtils.readJSON.mockReturnValueOnce({ tasks: [{ id: 1, title: 'Task 1', subtasks: [] }] }); const context = {}; await expect( addSubtask( 'tasks.json', '99', null, { title: 'New Subtask' }, true, context ) ).rejects.toThrow('Parent task with ID 99 not found'); }); test('should throw an error if trying to convert a non-existent task', async () => { mockUtils.readJSON.mockReturnValueOnce({ tasks: [{ id: 1, title: 'Parent Task', subtasks: [] }] }); const context = {}; await expect( addSubtask('tasks.json', '1', '99', null, true, context) ).rejects.toThrow('Task with ID 99 not found'); }); test('should throw an error for circular dependency', async () => { mockUtils.readJSON.mockReturnValueOnce({ tasks: [ { id: 1, title: 'Parent Task', subtasks: [] }, { id: 2, title: 'Child Task', subtasks: [] } ] }); mockTaskManager.isTaskDependentOn.mockImplementation( (tasks, parentTask, existingTaskIdNum) => { return parentTask.id === 1 && existingTaskIdNum === 2; } ); const context = {}; await expect( addSubtask('tasks.json', '1', '2', null, true, context) ).rejects.toThrow( 'Cannot create circular dependency: task 1 is already a subtask or dependent of task 2' ); }); }); ``` -------------------------------------------------------------------------------- /packages/tm-core/src/logger/logger.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview Core logger implementation */ import chalk from 'chalk'; export enum LogLevel { SILENT = 0, ERROR = 1, WARN = 2, INFO = 3, DEBUG = 4 } export interface LoggerConfig { level?: LogLevel; silent?: boolean; prefix?: string; timestamp?: boolean; colors?: boolean; // MCP mode silences all output mcpMode?: boolean; } export class Logger { private config: Required<LoggerConfig>; private static readonly DEFAULT_CONFIG: Required<LoggerConfig> = { level: LogLevel.WARN, silent: false, prefix: '', timestamp: false, colors: true, mcpMode: false }; constructor(config: LoggerConfig = {}) { // Check environment variables const envConfig: LoggerConfig = {}; // Check for MCP mode if ( process.env.MCP_MODE === 'true' || process.env.TASK_MASTER_MCP === 'true' ) { envConfig.mcpMode = true; } // Check for silent mode if ( process.env.TASK_MASTER_SILENT === 'true' || process.env.TM_SILENT === 'true' ) { envConfig.silent = true; } // Check for log level if (process.env.TASK_MASTER_LOG_LEVEL || process.env.TM_LOG_LEVEL) { const levelStr = ( process.env.TASK_MASTER_LOG_LEVEL || process.env.TM_LOG_LEVEL || '' ).toUpperCase(); if (levelStr in LogLevel) { envConfig.level = LogLevel[levelStr as keyof typeof LogLevel]; } } // Check for no colors if ( process.env.NO_COLOR === 'true' || process.env.TASK_MASTER_NO_COLOR === 'true' ) { envConfig.colors = false; } // Merge configs: defaults < constructor < environment this.config = { ...Logger.DEFAULT_CONFIG, ...config, ...envConfig }; // MCP mode overrides everything to be silent if (this.config.mcpMode) { this.config.silent = true; } } /** * Check if logging is enabled for a given level */ private shouldLog(level: LogLevel): boolean { if (this.config.silent || this.config.mcpMode) { return false; } return level <= this.config.level; } /** * Format a log message */ private formatMessage( level: LogLevel, message: string, ...args: any[] ): string { let formatted = ''; // Add timestamp if enabled if (this.config.timestamp) { const timestamp = new Date().toISOString(); formatted += this.config.colors ? chalk.gray(`[${timestamp}] `) : `[${timestamp}] `; } // Add prefix if configured if (this.config.prefix) { formatted += this.config.colors ? chalk.cyan(`[${this.config.prefix}] `) : `[${this.config.prefix}] `; } // Skip level indicator for cleaner output // We can still color the message based on level if (this.config.colors) { switch (level) { case LogLevel.ERROR: message = chalk.red(message); break; case LogLevel.WARN: message = chalk.yellow(message); break; case LogLevel.INFO: // Info stays default color break; case LogLevel.DEBUG: message = chalk.gray(message); break; } } // Add the message formatted += message; // Add any additional arguments if (args.length > 0) { formatted += ' ' + args .map((arg) => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg) ) .join(' '); } return formatted; } /** * Log an error message */ error(message: string, ...args: any[]): void { if (!this.shouldLog(LogLevel.ERROR)) return; console.error(this.formatMessage(LogLevel.ERROR, message, ...args)); } /** * Log a warning message */ warn(message: string, ...args: any[]): void { if (!this.shouldLog(LogLevel.WARN)) return; console.warn(this.formatMessage(LogLevel.WARN, message, ...args)); } /** * Log an info message */ info(message: string, ...args: any[]): void { if (!this.shouldLog(LogLevel.INFO)) return; console.log(this.formatMessage(LogLevel.INFO, message, ...args)); } /** * Log a debug message */ debug(message: string, ...args: any[]): void { if (!this.shouldLog(LogLevel.DEBUG)) return; console.log(this.formatMessage(LogLevel.DEBUG, message, ...args)); } /** * Log a message without any formatting (raw output) * Useful for CLI output that should appear as-is */ log(message: string, ...args: any[]): void { if (this.config.silent || this.config.mcpMode) return; if (args.length > 0) { console.log(message, ...args); } else { console.log(message); } } /** * Update logger configuration */ setConfig(config: Partial<LoggerConfig>): void { this.config = { ...this.config, ...config }; // MCP mode always overrides to silent if (this.config.mcpMode) { this.config.silent = true; } } /** * Get current configuration */ getConfig(): Readonly<Required<LoggerConfig>> { return { ...this.config }; } /** * Create a child logger with a prefix */ child(prefix: string, config?: Partial<LoggerConfig>): Logger { const childPrefix = this.config.prefix ? `${this.config.prefix}:${prefix}` : prefix; return new Logger({ ...this.config, ...config, prefix: childPrefix }); } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/add-tag.js: -------------------------------------------------------------------------------- ```javascript /** * add-tag.js * Direct function implementation for creating a new tag */ import { createTag, createTagFromBranch } from '../../../../scripts/modules/task-manager/tag-management.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; /** * Direct function wrapper for creating a new tag with error handling. * * @param {Object} args - Command arguments * @param {string} args.name - Name of the new tag to create * @param {boolean} [args.copyFromCurrent=false] - Whether to copy tasks from current tag * @param {string} [args.copyFromTag] - Specific tag to copy tasks from * @param {boolean} [args.fromBranch=false] - Create tag name from current git branch * @param {string} [args.description] - Optional description for the tag * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) * @param {string} [args.projectRoot] - Project root path * @param {Object} log - Logger object * @param {Object} context - Additional context (session) * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } */ export async function addTagDirect(args, log, context = {}) { // Destructure expected args const { tasksJsonPath, name, copyFromCurrent = false, copyFromTag, fromBranch = false, description, projectRoot } = args; const { session } = context; // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); // Create logger wrapper using the utility const mcpLog = createLogWrapper(log); try { // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('addTagDirect called without tasksJsonPath'); disableSilentMode(); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Handle --from-branch option if (fromBranch) { log.info('Creating tag from current git branch'); // Import git utilities const gitUtils = await import( '../../../../scripts/modules/utils/git-utils.js' ); // Check if we're in a git repository if (!(await gitUtils.isGitRepository(projectRoot))) { log.error('Not in a git repository'); disableSilentMode(); return { success: false, error: { code: 'NOT_GIT_REPO', message: 'Not in a git repository. Cannot use fromBranch option.' } }; } // Get current git branch const currentBranch = await gitUtils.getCurrentBranch(projectRoot); if (!currentBranch) { log.error('Could not determine current git branch'); disableSilentMode(); return { success: false, error: { code: 'NO_CURRENT_BRANCH', message: 'Could not determine current git branch.' } }; } // Prepare options for branch-based tag creation const branchOptions = { copyFromCurrent, copyFromTag, description: description || `Tag created from git branch "${currentBranch}"` }; // Call the createTagFromBranch function const result = await createTagFromBranch( tasksJsonPath, currentBranch, branchOptions, { session, mcpLog, projectRoot }, 'json' // outputFormat - use 'json' to suppress CLI UI ); // Restore normal logging disableSilentMode(); return { success: true, data: { branchName: result.branchName, tagName: result.tagName, created: result.created, mappingUpdated: result.mappingUpdated, message: `Successfully created tag "${result.tagName}" from git branch "${result.branchName}"` } }; } else { // Check required parameters for regular tag creation if (!name || typeof name !== 'string') { log.error('Missing required parameter: name'); disableSilentMode(); return { success: false, error: { code: 'MISSING_PARAMETER', message: 'Tag name is required and must be a string' } }; } log.info(`Creating new tag: ${name}`); // Prepare options const options = { copyFromCurrent, copyFromTag, description }; // Call the createTag function const result = await createTag( tasksJsonPath, name, options, { session, mcpLog, projectRoot }, 'json' // outputFormat - use 'json' to suppress CLI UI ); // Restore normal logging disableSilentMode(); return { success: true, data: { tagName: result.tagName, created: result.created, tasksCopied: result.tasksCopied, sourceTag: result.sourceTag, description: result.description, message: `Successfully created tag "${result.tagName}"` } }; } } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error in addTagDirect: ${error.message}`); return { success: false, error: { code: error.code || 'ADD_TAG_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/components/EmptyState.tsx: -------------------------------------------------------------------------------- ```typescript import React from 'react'; import { ExternalLink, Terminal, MessageSquare, Plus } from 'lucide-react'; import { TaskMasterLogo } from '../../components/TaskMasterLogo'; interface EmptyStateProps { currentTag: string; } export const EmptyState: React.FC<EmptyStateProps> = ({ currentTag }) => { return ( <div className="flex items-center justify-center h-full overflow-auto"> <div className="max-w-2xl mx-auto text-center p-8"> {/* Empty state illustration */} <div className="mb-8 max-w-96 mx-auto"> <TaskMasterLogo className="w-32 h-32 mx-auto text-vscode-foreground/20" /> </div> <h2 className="text-2xl font-semibold mb-2 text-vscode-foreground"> No tasks in "{currentTag}" tag </h2> <p className="text-vscode-foreground/70 mb-8"> Get started by adding tasks to this tag using the commands below </p> {/* Command suggestions */} <div className="space-y-4 text-left"> <div className="bg-vscode-editor-background/50 border border-vscode-panel-border rounded-lg p-4"> <div className="flex items-center gap-2 mb-2"> <Terminal className="w-4 h-4 text-vscode-terminal-ansiGreen" /> <h3 className="font-medium">CLI Commands</h3> </div> <div className="space-y-2"> <div className="bg-vscode-editor-background rounded p-2 font-mono text-sm"> <span className="text-vscode-terminal-ansiYellow"> task-master </span>{' '} <span className="text-vscode-terminal-ansiCyan">parse-prd</span>{' '} <span className="text-vscode-foreground/70"> <path-to-prd> </span>{' '} <span className="text-vscode-terminal-ansiMagenta"> --append </span> <div className="text-xs text-vscode-foreground/50 mt-1"> Parse a PRD and append tasks to current tag </div> </div> <div className="bg-vscode-editor-background rounded p-2 font-mono text-sm"> <span className="text-vscode-terminal-ansiYellow"> task-master </span>{' '} <span className="text-vscode-terminal-ansiCyan">add-task</span>{' '} <span className="text-vscode-terminal-ansiMagenta"> --prompt </span>{' '} <span className="text-vscode-foreground/70"> "Your task description" </span> <div className="text-xs text-vscode-foreground/50 mt-1"> Add a single task with AI assistance </div> </div> <div className="bg-vscode-editor-background rounded p-2 font-mono text-sm"> <span className="text-vscode-terminal-ansiYellow"> task-master </span>{' '} <span className="text-vscode-terminal-ansiCyan">add-task</span>{' '} <span className="text-vscode-terminal-ansiMagenta">--help</span> <div className="text-xs text-vscode-foreground/50 mt-1"> View all options for adding tasks </div> </div> </div> </div> <div className="bg-vscode-editor-background/50 border border-vscode-panel-border rounded-lg p-4"> <div className="flex items-center gap-2 mb-2"> <MessageSquare className="w-4 h-4 text-vscode-textLink-foreground" /> <h3 className="font-medium">MCP Examples</h3> </div> <div className="space-y-2 text-sm"> <div className="flex items-start gap-2"> <Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" /> <div> <div className="text-vscode-foreground"> "Add a task to tag {currentTag}: Implement user authentication" </div> </div> </div> <div className="flex items-start gap-2"> <Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" /> <div> <div className="text-vscode-foreground"> "Parse this PRD and add tasks to {currentTag}: [paste PRD content]" </div> </div> </div> <div className="flex items-start gap-2"> <Plus className="w-4 h-4 mt-0.5 text-vscode-foreground/50" /> <div> <div className="text-vscode-foreground"> "Create 5 tasks for building a REST API in tag {currentTag}" </div> </div> </div> </div> </div> {/* Documentation link */} <div className="flex justify-center pt-4"> <a href="https://docs.task-master.dev" className="inline-flex items-center gap-2 text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground transition-colors" onClick={(e) => { e.preventDefault(); // Use VS Code API to open external link if (window.acquireVsCodeApi) { const vscode = window.acquireVsCodeApi(); vscode.postMessage({ type: 'openExternal', url: 'https://docs.task-master.dev' }); } }} > <ExternalLink className="w-4 h-4" /> <span className="text-sm font-medium"> View TaskMaster Documentation </span> </a> </div> </div> </div> </div> ); }; ``` -------------------------------------------------------------------------------- /tests/unit/profiles/roo-integration.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; import fs from 'fs'; import path from 'path'; import os from 'os'; // Mock external modules jest.mock('child_process', () => ({ execSync: jest.fn() })); // Mock console methods jest.mock('console', () => ({ log: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), clear: jest.fn() })); describe('Roo Integration', () => { let tempDir; beforeEach(() => { jest.clearAllMocks(); // Create a temporary directory for testing tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); // Spy on fs methods jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { if (filePath.toString().includes('.roomodes')) { return 'Existing roomodes content'; } if (filePath.toString().includes('-rules')) { return 'Existing mode rules content'; } return '{}'; }); jest.spyOn(fs, 'existsSync').mockImplementation(() => false); jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); }); afterEach(() => { // Clean up the temporary directory try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (err) { console.error(`Error cleaning up: ${err.message}`); } }); // Test function that simulates the createProjectStructure behavior for Roo files function mockCreateRooStructure() { // Create main .roo directory fs.mkdirSync(path.join(tempDir, '.roo'), { recursive: true }); // Create rules directory fs.mkdirSync(path.join(tempDir, '.roo', 'rules'), { recursive: true }); // Create mode-specific rule directories const rooModes = [ 'architect', 'ask', 'orchestrator', 'code', 'debug', 'test' ]; for (const mode of rooModes) { fs.mkdirSync(path.join(tempDir, '.roo', `rules-${mode}`), { recursive: true }); fs.writeFileSync( path.join(tempDir, '.roo', `rules-${mode}`, `${mode}-rules`), `Content for ${mode} rules` ); } // Create additional directories fs.mkdirSync(path.join(tempDir, '.roo', 'config'), { recursive: true }); fs.mkdirSync(path.join(tempDir, '.roo', 'templates'), { recursive: true }); fs.mkdirSync(path.join(tempDir, '.roo', 'logs'), { recursive: true }); // Copy .roomodes file fs.writeFileSync(path.join(tempDir, '.roomodes'), 'Roomodes file content'); } test('creates all required .roo directories', () => { // Act mockCreateRooStructure(); // Assert expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.roo'), { recursive: true }); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules'), { recursive: true } ); // Verify all mode directories are created expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-architect'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-ask'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-orchestrator'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-code'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-debug'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-test'), { recursive: true } ); }); test('creates rule files for all modes', () => { // Act mockCreateRooStructure(); // Assert - check all rule files are created expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-architect', 'architect-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-ask', 'ask-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-orchestrator', 'orchestrator-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-code', 'code-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-debug', 'debug-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'rules-test', 'test-rules'), expect.any(String) ); }); test('creates .roomodes file in project root', () => { // Act mockCreateRooStructure(); // Assert expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.roomodes'), expect.any(String) ); }); test('creates additional required Roo directories', () => { // Act mockCreateRooStructure(); // Assert expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'config'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'templates'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.roo', 'logs'), { recursive: true } ); }); }); ``` -------------------------------------------------------------------------------- /apps/extension/src/components/TaskDetails/DetailsSection.tsx: -------------------------------------------------------------------------------- ```typescript import type React from 'react'; import { CollapsibleSection } from '@/components/ui/CollapsibleSection'; interface MarkdownRendererProps { content: string; className?: string; } const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, className = '' }) => { const parseMarkdown = (text: string) => { const parts = []; const lines = text.split('\n'); let currentBlock = []; let inCodeBlock = false; let codeLanguage = ''; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('```')) { if (inCodeBlock) { if (currentBlock.length > 0) { parts.push({ type: 'code', content: currentBlock.join('\n'), language: codeLanguage }); currentBlock = []; } inCodeBlock = false; codeLanguage = ''; } else { if (currentBlock.length > 0) { parts.push({ type: 'text', content: currentBlock.join('\n') }); currentBlock = []; } inCodeBlock = true; codeLanguage = line.substring(3).trim(); } } else { currentBlock.push(line); } } if (currentBlock.length > 0) { parts.push({ type: inCodeBlock ? 'code' : 'text', content: currentBlock.join('\n'), language: codeLanguage }); } return parts; }; const parts = parseMarkdown(content); return ( <div className={className}> {parts.map((part, index) => { if (part.type === 'code') { return ( <pre key={index} className="bg-vscode-editor-background rounded-md p-4 overflow-x-auto mb-4 border border-vscode-editor-lineHighlightBorder" > <code className="text-sm text-vscode-editor-foreground font-mono"> {part.content} </code> </pre> ); } return ( <div key={index} className="whitespace-pre-wrap mb-4 last:mb-0"> {part.content.split('\n').map((line, lineIndex) => { const bulletMatch = line.match(/^(\s*)([-*])\s(.+)$/); if (bulletMatch) { const indent = bulletMatch[1].length; return ( <div key={lineIndex} className="flex gap-2 mb-1" style={{ paddingLeft: `${indent * 16}px` }} > <span className="text-vscode-foreground/60">•</span> <span className="flex-1">{bulletMatch[3]}</span> </div> ); } const numberedMatch = line.match(/^(\s*)(\d+\.)\s(.+)$/); if (numberedMatch) { const indent = numberedMatch[1].length; return ( <div key={lineIndex} className="flex gap-2 mb-1" style={{ paddingLeft: `${indent * 16}px` }} > <span className="text-vscode-foreground/60 font-mono"> {numberedMatch[2]} </span> <span className="flex-1">{numberedMatch[3]}</span> </div> ); } const headingMatch = line.match(/^(#{1,6})\s(.+)$/); if (headingMatch) { const level = headingMatch[1].length; const headingLevel = Math.min(level + 2, 6); const headingClassName = 'font-semibold text-vscode-foreground mb-2 mt-4 first:mt-0'; switch (headingLevel) { case 3: return ( <h3 key={lineIndex} className={headingClassName}> {headingMatch[2]} </h3> ); case 4: return ( <h4 key={lineIndex} className={headingClassName}> {headingMatch[2]} </h4> ); case 5: return ( <h5 key={lineIndex} className={headingClassName}> {headingMatch[2]} </h5> ); case 6: return ( <h6 key={lineIndex} className={headingClassName}> {headingMatch[2]} </h6> ); default: return ( <h3 key={lineIndex} className={headingClassName}> {headingMatch[2]} </h3> ); } } if (line.trim() === '') { return <div key={lineIndex} className="h-2" />; } return ( <div key={lineIndex} className="mb-2 last:mb-0"> {line} </div> ); })} </div> ); })} </div> ); }; interface DetailsSectionProps { title: string; content?: string; error?: string | null; emptyMessage?: string; defaultExpanded?: boolean; } export const DetailsSection: React.FC<DetailsSectionProps> = ({ title, content, error, emptyMessage = 'No details available', defaultExpanded = false }) => { return ( <CollapsibleSection title={title} defaultExpanded={defaultExpanded}> <div className={title.toLowerCase().replace(/\s+/g, '-') + '-content'}> {error ? ( <div className="text-sm text-red-400 py-2"> Error loading {title.toLowerCase()}: {error} </div> ) : content !== undefined && content !== '' ? ( <MarkdownRenderer content={content} /> ) : ( <div className="text-sm text-vscode-foreground/50 py-2"> {emptyMessage} </div> )} </div> </CollapsibleSection> ); }; ``` -------------------------------------------------------------------------------- /src/utils/timeout-manager.js: -------------------------------------------------------------------------------- ```javascript import { StreamingError, STREAMING_ERROR_CODES } from './stream-parser.js'; /** * Utility class for managing timeouts in async operations * Reduces code duplication for timeout handling patterns */ export class TimeoutManager { /** * Wraps a promise with a timeout that will reject if not resolved in time * * @param {Promise} promise - The promise to wrap with timeout * @param {number} timeoutMs - Timeout duration in milliseconds * @param {string} operationName - Name of the operation for error messages * @returns {Promise} The result of the promise or throws timeout error * * @example * const result = await TimeoutManager.withTimeout( * fetchData(), * 5000, * 'Data fetch operation' * ); */ static async withTimeout(promise, timeoutMs, operationName = 'Operation') { let timeoutHandle; const timeoutPromise = new Promise((_, reject) => { timeoutHandle = setTimeout(() => { reject( new StreamingError( `${operationName} timed out after ${timeoutMs / 1000} seconds`, STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED ) ); }, timeoutMs); }); try { // Race between the actual promise and the timeout const result = await Promise.race([promise, timeoutPromise]); // Clear timeout if promise resolved first clearTimeout(timeoutHandle); return result; } catch (error) { // Always clear timeout on error clearTimeout(timeoutHandle); throw error; } } /** * Wraps a promise with a timeout, but returns undefined instead of throwing on timeout * Useful for optional operations that shouldn't fail the main flow * * @param {Promise} promise - The promise to wrap with timeout * @param {number} timeoutMs - Timeout duration in milliseconds * @param {*} defaultValue - Value to return on timeout (default: undefined) * @returns {Promise} The result of the promise or defaultValue on timeout * * @example * const usage = await TimeoutManager.withSoftTimeout( * getUsageStats(), * 1000, * { tokens: 0 } * ); */ static async withSoftTimeout(promise, timeoutMs, defaultValue = undefined) { let timeoutHandle; const timeoutPromise = new Promise((resolve) => { timeoutHandle = setTimeout(() => { resolve(defaultValue); }, timeoutMs); }); try { const result = await Promise.race([promise, timeoutPromise]); clearTimeout(timeoutHandle); return result; } catch (error) { // On error, clear timeout and return default value clearTimeout(timeoutHandle); return defaultValue; } } /** * Creates a reusable timeout controller for multiple operations * Useful when you need to apply the same timeout to multiple promises * * @param {number} timeoutMs - Timeout duration in milliseconds * @param {string} operationName - Base name for operations * @returns {Object} Controller with wrap method * * @example * const controller = TimeoutManager.createController(60000, 'AI Service'); * const result1 = await controller.wrap(service.call1(), 'call 1'); * const result2 = await controller.wrap(service.call2(), 'call 2'); */ static createController(timeoutMs, operationName = 'Operation') { return { timeoutMs, operationName, async wrap(promise, specificName = null) { const fullName = specificName ? `${operationName} - ${specificName}` : operationName; return TimeoutManager.withTimeout(promise, timeoutMs, fullName); }, async wrapSoft(promise, defaultValue = undefined) { return TimeoutManager.withSoftTimeout(promise, timeoutMs, defaultValue); } }; } /** * Checks if an error is a timeout error from this manager * * @param {Error} error - The error to check * @returns {boolean} True if this is a timeout error */ static isTimeoutError(error) { return ( error instanceof StreamingError && error.code === STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED && error.message.includes('timed out') ); } } /** * Duration helper class for more readable timeout specifications */ export class Duration { constructor(value, unit = 'ms') { this.milliseconds = this._toMilliseconds(value, unit); } static milliseconds(value) { return new Duration(value, 'ms'); } static seconds(value) { return new Duration(value, 's'); } static minutes(value) { return new Duration(value, 'm'); } static hours(value) { return new Duration(value, 'h'); } get seconds() { return this.milliseconds / 1000; } get minutes() { return this.milliseconds / 60000; } get hours() { return this.milliseconds / 3600000; } toString() { if (this.milliseconds < 1000) { return `${this.milliseconds}ms`; } else if (this.milliseconds < 60000) { return `${this.seconds}s`; } else if (this.milliseconds < 3600000) { return `${Math.floor(this.minutes)}m ${Math.floor(this.seconds % 60)}s`; } else { return `${Math.floor(this.hours)}h ${Math.floor(this.minutes % 60)}m`; } } _toMilliseconds(value, unit) { const conversions = { ms: 1, s: 1000, m: 60000, h: 3600000 }; return value * (conversions[unit] || 1); } } ``` -------------------------------------------------------------------------------- /tests/unit/profiles/kilo-integration.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; import fs from 'fs'; import path from 'path'; import os from 'os'; // Mock external modules jest.mock('child_process', () => ({ execSync: jest.fn() })); // Mock console methods jest.mock('console', () => ({ log: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), clear: jest.fn() })); describe('Kilo Integration', () => { let tempDir; beforeEach(() => { jest.clearAllMocks(); // Create a temporary directory for testing tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); // Spy on fs methods jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { if (filePath.toString().includes('.kilocodemodes')) { return 'Existing kilocodemodes content'; } if (filePath.toString().includes('-rules')) { return 'Existing mode rules content'; } return '{}'; }); jest.spyOn(fs, 'existsSync').mockImplementation(() => false); jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); }); afterEach(() => { // Clean up the temporary directory try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (err) { console.error(`Error cleaning up: ${err.message}`); } }); // Test function that simulates the createProjectStructure behavior for Kilo files function mockCreateKiloStructure() { // Create main .kilo directory fs.mkdirSync(path.join(tempDir, '.kilo'), { recursive: true }); // Create rules directory fs.mkdirSync(path.join(tempDir, '.kilo', 'rules'), { recursive: true }); // Create mode-specific rule directories const kiloModes = [ 'architect', 'ask', 'orchestrator', 'code', 'debug', 'test' ]; for (const mode of kiloModes) { fs.mkdirSync(path.join(tempDir, '.kilo', `rules-${mode}`), { recursive: true }); fs.writeFileSync( path.join(tempDir, '.kilo', `rules-${mode}`, `${mode}-rules`), `Content for ${mode} rules` ); } // Create additional directories fs.mkdirSync(path.join(tempDir, '.kilo', 'config'), { recursive: true }); fs.mkdirSync(path.join(tempDir, '.kilo', 'templates'), { recursive: true }); fs.mkdirSync(path.join(tempDir, '.kilo', 'logs'), { recursive: true }); // Copy .kilocodemodes file fs.writeFileSync( path.join(tempDir, '.kilocodemodes'), 'Kilocodemodes file content' ); } test('creates all required .kilo directories', () => { // Act mockCreateKiloStructure(); // Assert expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.kilo'), { recursive: true }); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules'), { recursive: true } ); // Verify all mode directories are created expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-architect'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-ask'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-orchestrator'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-code'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-debug'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-test'), { recursive: true } ); }); test('creates rule files for all modes', () => { // Act mockCreateKiloStructure(); // Assert - check all rule files are created expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-architect', 'architect-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-ask', 'ask-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-orchestrator', 'orchestrator-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-code', 'code-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-debug', 'debug-rules'), expect.any(String) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'rules-test', 'test-rules'), expect.any(String) ); }); test('creates .kilocodemodes file in project root', () => { // Act mockCreateKiloStructure(); // Assert expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kilocodemodes'), expect.any(String) ); }); test('creates additional required Kilo directories', () => { // Act mockCreateKiloStructure(); // Assert expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'config'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'templates'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kilo', 'logs'), { recursive: true } ); }); }); ```