This is page 7 of 52. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /tests/integration/profiles/cline-init-functionality.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { clineProfile } from '../../../src/profiles/cline.js'; 4 | 5 | describe('Cline Profile Initialization Functionality', () => { 6 | let clineProfileContent; 7 | 8 | beforeAll(() => { 9 | const clineJsPath = path.join(process.cwd(), 'src', 'profiles', 'cline.js'); 10 | clineProfileContent = fs.readFileSync(clineJsPath, 'utf8'); 11 | }); 12 | 13 | test('cline.js uses factory pattern with correct configuration', () => { 14 | // Check for explicit, non-default values in the source file 15 | expect(clineProfileContent).toContain("name: 'cline'"); 16 | expect(clineProfileContent).toContain("displayName: 'Cline'"); 17 | expect(clineProfileContent).toContain("profileDir: '.clinerules'"); // non-default 18 | expect(clineProfileContent).toContain("rulesDir: '.clinerules'"); // non-default 19 | expect(clineProfileContent).toContain('mcpConfig: false'); // non-default 20 | 21 | // Check the final computed properties on the profile object 22 | expect(clineProfile.profileName).toBe('cline'); 23 | expect(clineProfile.displayName).toBe('Cline'); 24 | expect(clineProfile.profileDir).toBe('.clinerules'); 25 | expect(clineProfile.rulesDir).toBe('.clinerules'); 26 | expect(clineProfile.mcpConfig).toBe(false); 27 | expect(clineProfile.mcpConfigName).toBe(null); 28 | }); 29 | 30 | test('cline.js configures .mdc to .md extension mapping', () => { 31 | // Check that the profile object has the correct file mapping behavior (cline converts to .md) 32 | expect(clineProfile.fileMap['rules/cursor_rules.mdc']).toBe( 33 | 'cline_rules.md' 34 | ); 35 | }); 36 | 37 | test('cline.js uses standard tool mappings', () => { 38 | // Check that the profile uses default tool mappings (equivalent to COMMON_TOOL_MAPPINGS.STANDARD) 39 | // This verifies the architectural pattern: no custom toolMappings = standard tool names 40 | expect(clineProfileContent).not.toContain('toolMappings:'); 41 | expect(clineProfileContent).not.toContain('apply_diff'); 42 | expect(clineProfileContent).not.toContain('search_files'); 43 | 44 | // Verify the result: default mappings means tools keep their original names 45 | expect(clineProfile.conversionConfig.toolNames.edit_file).toBe('edit_file'); 46 | expect(clineProfile.conversionConfig.toolNames.search).toBe('search'); 47 | }); 48 | 49 | test('cline.js has custom file mapping for cursor_rules.mdc', () => { 50 | // Check actual behavior - cline gets default rule files 51 | expect(Object.keys(clineProfile.fileMap)).toContain( 52 | 'rules/cursor_rules.mdc' 53 | ); 54 | expect(clineProfile.fileMap['rules/cursor_rules.mdc']).toBe( 55 | 'cline_rules.md' 56 | ); 57 | }); 58 | 59 | test('cline.js uses createProfile factory function', () => { 60 | expect(clineProfileContent).toContain('createProfile'); 61 | expect(clineProfileContent).toContain('export const clineProfile'); 62 | }); 63 | }); 64 | ``` -------------------------------------------------------------------------------- /packages/tm-core/src/executors/executor-service.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Service for managing task execution 3 | */ 4 | 5 | import type { Task } from '../types/index.js'; 6 | import type { 7 | ITaskExecutor, 8 | ExecutorOptions, 9 | ExecutionResult, 10 | ExecutorType 11 | } from './types.js'; 12 | import { ExecutorFactory } from './executor-factory.js'; 13 | import { getLogger } from '../logger/index.js'; 14 | 15 | export interface ExecutorServiceOptions { 16 | projectRoot: string; 17 | defaultExecutor?: ExecutorType; 18 | executorConfig?: Record<string, any>; 19 | } 20 | 21 | export class ExecutorService { 22 | private logger = getLogger('ExecutorService'); 23 | private projectRoot: string; 24 | private defaultExecutor?: ExecutorType; 25 | private executorConfig: Record<string, any>; 26 | private currentExecutor?: ITaskExecutor; 27 | 28 | constructor(options: ExecutorServiceOptions) { 29 | this.projectRoot = options.projectRoot; 30 | this.defaultExecutor = options.defaultExecutor; 31 | this.executorConfig = options.executorConfig || {}; 32 | } 33 | 34 | /** 35 | * Execute a task 36 | */ 37 | async executeTask( 38 | task: Task, 39 | executorType?: ExecutorType 40 | ): Promise<ExecutionResult> { 41 | try { 42 | // Determine executor type 43 | const type = 44 | executorType || 45 | this.defaultExecutor || 46 | (await ExecutorFactory.getDefaultExecutor(this.projectRoot)); 47 | if (!type) { 48 | return { 49 | success: false, 50 | taskId: task.id, 51 | executorType: 'claude', 52 | error: 53 | 'No executor available. Please install Claude CLI or specify an executor type.', 54 | startTime: new Date().toISOString() 55 | }; 56 | } 57 | 58 | // Create executor 59 | const executorOptions: ExecutorOptions = { 60 | type, 61 | projectRoot: this.projectRoot, 62 | config: this.executorConfig 63 | }; 64 | 65 | this.currentExecutor = ExecutorFactory.create(executorOptions); 66 | 67 | // Check if executor is available 68 | const isAvailable = await this.currentExecutor.isAvailable(); 69 | if (!isAvailable) { 70 | return { 71 | success: false, 72 | taskId: task.id, 73 | executorType: type, 74 | error: `Executor ${type} is not available or not configured properly`, 75 | startTime: new Date().toISOString() 76 | }; 77 | } 78 | 79 | // Execute the task 80 | this.logger.info(`Starting task ${task.id} with ${type} executor`); 81 | const result = await this.currentExecutor.execute(task); 82 | 83 | return result; 84 | } catch (error: any) { 85 | this.logger.error(`Failed to execute task ${task.id}:`, error); 86 | return { 87 | success: false, 88 | taskId: task.id, 89 | executorType: executorType || 'claude', 90 | error: error.message || 'Unknown error occurred', 91 | startTime: new Date().toISOString() 92 | }; 93 | } 94 | } 95 | 96 | /** 97 | * Stop the current task execution 98 | */ 99 | async stopCurrentTask(): Promise<void> { 100 | if (this.currentExecutor && this.currentExecutor.stop) { 101 | await this.currentExecutor.stop(); 102 | this.currentExecutor = undefined; 103 | } 104 | } 105 | } 106 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/remove-task.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/remove-task.js 3 | * Tool to remove a task by ID 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | handleApiResult, 9 | createErrorResponse, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { removeTaskDirect } from '../core/task-master-core.js'; 13 | import { findTasksPath } from '../core/utils/path-utils.js'; 14 | import { resolveTag } from '../../../scripts/modules/utils.js'; 15 | 16 | /** 17 | * Register the remove-task tool with the MCP server 18 | * @param {Object} server - FastMCP server instance 19 | */ 20 | export function registerRemoveTaskTool(server) { 21 | server.addTool({ 22 | name: 'remove_task', 23 | description: 'Remove a task or subtask permanently from the tasks list', 24 | parameters: z.object({ 25 | id: z 26 | .string() 27 | .describe( 28 | "ID of the task or subtask to remove (e.g., '5' or '5.2'). Can be comma-separated to update multiple tasks/subtasks at once." 29 | ), 30 | file: z.string().optional().describe('Absolute path to the tasks file'), 31 | projectRoot: z 32 | .string() 33 | .describe('The directory of the project. Must be an absolute path.'), 34 | confirm: z 35 | .boolean() 36 | .optional() 37 | .describe('Whether to skip confirmation prompt (default: false)'), 38 | tag: z 39 | .string() 40 | .optional() 41 | .describe( 42 | 'Specify which tag context to operate on. Defaults to the current active tag.' 43 | ) 44 | }), 45 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 46 | try { 47 | log.info(`Removing task(s) with ID(s): ${args.id}`); 48 | 49 | const resolvedTag = resolveTag({ 50 | projectRoot: args.projectRoot, 51 | tag: args.tag 52 | }); 53 | 54 | // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) 55 | let tasksJsonPath; 56 | try { 57 | tasksJsonPath = findTasksPath( 58 | { projectRoot: args.projectRoot, file: args.file }, 59 | log 60 | ); 61 | } catch (error) { 62 | log.error(`Error finding tasks.json: ${error.message}`); 63 | return createErrorResponse( 64 | `Failed to find tasks.json: ${error.message}` 65 | ); 66 | } 67 | 68 | log.info(`Using tasks file path: ${tasksJsonPath}`); 69 | 70 | const result = await removeTaskDirect( 71 | { 72 | tasksJsonPath: tasksJsonPath, 73 | id: args.id, 74 | projectRoot: args.projectRoot, 75 | tag: resolvedTag 76 | }, 77 | log, 78 | { session } 79 | ); 80 | 81 | if (result.success) { 82 | log.info(`Successfully removed task: ${args.id}`); 83 | } else { 84 | log.error(`Failed to remove task: ${result.error.message}`); 85 | } 86 | 87 | return handleApiResult( 88 | result, 89 | log, 90 | 'Error removing task', 91 | undefined, 92 | args.projectRoot 93 | ); 94 | } catch (error) { 95 | log.error(`Error in remove-task tool: ${error.message}`); 96 | return createErrorResponse(`Failed to remove task: ${error.message}`); 97 | } 98 | }) 99 | }); 100 | } 101 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/scope-up.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/scope-up.js 3 | * Tool to scope up task complexity 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | createErrorResponse, 9 | handleApiResult, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { scopeUpDirect } from '../core/task-master-core.js'; 13 | import { findTasksPath } from '../core/utils/path-utils.js'; 14 | import { resolveTag } from '../../../scripts/modules/utils.js'; 15 | 16 | /** 17 | * Register the scopeUp tool with the MCP server 18 | * @param {Object} server - FastMCP server instance 19 | */ 20 | export function registerScopeUpTool(server) { 21 | server.addTool({ 22 | name: 'scope_up_task', 23 | description: 'Increase the complexity of one or more tasks using AI', 24 | parameters: z.object({ 25 | id: z 26 | .string() 27 | .describe( 28 | 'Comma-separated list of task IDs to scope up (e.g., "1,3,5")' 29 | ), 30 | strength: z 31 | .string() 32 | .optional() 33 | .describe( 34 | 'Strength level: light, regular, or heavy (default: regular)' 35 | ), 36 | prompt: z 37 | .string() 38 | .optional() 39 | .describe('Custom prompt for specific scoping adjustments'), 40 | file: z 41 | .string() 42 | .optional() 43 | .describe('Path to the tasks file (default: tasks/tasks.json)'), 44 | projectRoot: z 45 | .string() 46 | .describe('The directory of the project. Must be an absolute path.'), 47 | tag: z.string().optional().describe('Tag context to operate on'), 48 | research: z 49 | .boolean() 50 | .optional() 51 | .describe('Whether to use research capabilities for scoping') 52 | }), 53 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 54 | try { 55 | log.info(`Starting scope-up with args: ${JSON.stringify(args)}`); 56 | 57 | const resolvedTag = resolveTag({ 58 | projectRoot: args.projectRoot, 59 | tag: args.tag 60 | }); 61 | 62 | // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) 63 | let tasksJsonPath; 64 | try { 65 | tasksJsonPath = findTasksPath( 66 | { projectRoot: args.projectRoot, file: args.file }, 67 | log 68 | ); 69 | } catch (error) { 70 | log.error(`Error finding tasks.json: ${error.message}`); 71 | return createErrorResponse( 72 | `Failed to find tasks.json: ${error.message}` 73 | ); 74 | } 75 | 76 | // Call the direct function 77 | const result = await scopeUpDirect( 78 | { 79 | tasksJsonPath: tasksJsonPath, 80 | id: args.id, 81 | strength: args.strength, 82 | prompt: args.prompt, 83 | research: args.research, 84 | projectRoot: args.projectRoot, 85 | tag: resolvedTag 86 | }, 87 | log, 88 | { session } 89 | ); 90 | 91 | return handleApiResult( 92 | result, 93 | log, 94 | 'Error scoping up task', 95 | undefined, 96 | args.projectRoot 97 | ); 98 | } catch (error) { 99 | log.error(`Error in scope-up tool: ${error.message}`); 100 | return createErrorResponse(error.message); 101 | } 102 | }) 103 | }); 104 | } 105 | ``` -------------------------------------------------------------------------------- /mcp-server/src/custom-sdk/message-converter.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * src/ai-providers/custom-sdk/mcp/message-converter.js 3 | * 4 | * Message conversion utilities for converting between AI SDK prompt format 5 | * and MCP sampling format. 6 | */ 7 | 8 | /** 9 | * Convert AI SDK prompt format to MCP sampling format 10 | * @param {Array} prompt - AI SDK prompt array 11 | * @returns {object} MCP format with messages and systemPrompt 12 | */ 13 | export function convertToMCPFormat(prompt) { 14 | const messages = []; 15 | let systemPrompt = ''; 16 | 17 | for (const message of prompt) { 18 | if (message.role === 'system') { 19 | // Extract system prompt 20 | systemPrompt = extractTextContent(message.content); 21 | } else if (message.role === 'user' || message.role === 'assistant') { 22 | // Convert user/assistant messages 23 | messages.push({ 24 | role: message.role, 25 | content: { 26 | type: 'text', 27 | text: extractTextContent(message.content) 28 | } 29 | }); 30 | } 31 | } 32 | 33 | return { 34 | messages, 35 | systemPrompt 36 | }; 37 | } 38 | 39 | /** 40 | * Convert MCP response format to AI SDK format 41 | * @param {object} response - MCP sampling response 42 | * @returns {object} AI SDK compatible result 43 | */ 44 | export function convertFromMCPFormat(response) { 45 | // Handle different possible response formats 46 | let text = ''; 47 | let usage = null; 48 | let finishReason = 'stop'; 49 | let warnings = []; 50 | 51 | if (typeof response === 'string') { 52 | text = response; 53 | } else if (response.content) { 54 | text = extractTextContent(response.content); 55 | usage = response.usage; 56 | finishReason = response.finishReason || 'stop'; 57 | } else if (response.text) { 58 | text = response.text; 59 | usage = response.usage; 60 | finishReason = response.finishReason || 'stop'; 61 | } else { 62 | // Fallback: try to extract text from response 63 | text = JSON.stringify(response); 64 | warnings.push('Unexpected MCP response format, used JSON fallback'); 65 | } 66 | 67 | return { 68 | text, 69 | usage, 70 | finishReason, 71 | warnings 72 | }; 73 | } 74 | 75 | /** 76 | * Extract text content from various content formats 77 | * @param {string|Array|object} content - Content in various formats 78 | * @returns {string} Extracted text 79 | */ 80 | function extractTextContent(content) { 81 | if (typeof content === 'string') { 82 | return content; 83 | } 84 | 85 | if (Array.isArray(content)) { 86 | // Handle array of content parts 87 | return content 88 | .map((part) => { 89 | if (typeof part === 'string') { 90 | return part; 91 | } 92 | if (part.type === 'text' && part.text) { 93 | return part.text; 94 | } 95 | if (part.text) { 96 | return part.text; 97 | } 98 | // Skip non-text content (images, etc.) 99 | return ''; 100 | }) 101 | .filter((text) => text.length > 0) 102 | .join(' '); 103 | } 104 | 105 | if (content && typeof content === 'object') { 106 | if (content.type === 'text' && content.text) { 107 | return content.text; 108 | } 109 | if (content.text) { 110 | return content.text; 111 | } 112 | } 113 | 114 | // Fallback 115 | return String(content || ''); 116 | } 117 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/use-tag.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * use-tag.js 3 | * Direct function implementation for switching to a tag 4 | */ 5 | 6 | import { useTag } from '../../../../scripts/modules/task-manager/tag-management.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode 10 | } from '../../../../scripts/modules/utils.js'; 11 | import { createLogWrapper } from '../../tools/utils.js'; 12 | 13 | /** 14 | * Direct function wrapper for switching to a tag with error handling. 15 | * 16 | * @param {Object} args - Command arguments 17 | * @param {string} args.name - Name of the tag to switch to 18 | * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) 19 | * @param {string} [args.projectRoot] - Project root path 20 | * @param {Object} log - Logger object 21 | * @param {Object} context - Additional context (session) 22 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } 23 | */ 24 | export async function useTagDirect(args, log, context = {}) { 25 | // Destructure expected args 26 | const { tasksJsonPath, name, projectRoot } = args; 27 | const { session } = context; 28 | 29 | // Enable silent mode to prevent console logs from interfering with JSON response 30 | enableSilentMode(); 31 | 32 | // Create logger wrapper using the utility 33 | const mcpLog = createLogWrapper(log); 34 | 35 | try { 36 | // Check if tasksJsonPath was provided 37 | if (!tasksJsonPath) { 38 | log.error('useTagDirect called without tasksJsonPath'); 39 | disableSilentMode(); 40 | return { 41 | success: false, 42 | error: { 43 | code: 'MISSING_ARGUMENT', 44 | message: 'tasksJsonPath is required' 45 | } 46 | }; 47 | } 48 | 49 | // Check required parameters 50 | if (!name || typeof name !== 'string') { 51 | log.error('Missing required parameter: name'); 52 | disableSilentMode(); 53 | return { 54 | success: false, 55 | error: { 56 | code: 'MISSING_PARAMETER', 57 | message: 'Tag name is required and must be a string' 58 | } 59 | }; 60 | } 61 | 62 | log.info(`Switching to tag: ${name}`); 63 | 64 | // Call the useTag function 65 | const result = await useTag( 66 | tasksJsonPath, 67 | name, 68 | {}, // options (empty for now) 69 | { 70 | session, 71 | mcpLog, 72 | projectRoot 73 | }, 74 | 'json' // outputFormat - use 'json' to suppress CLI UI 75 | ); 76 | 77 | // Restore normal logging 78 | disableSilentMode(); 79 | 80 | return { 81 | success: true, 82 | data: { 83 | tagName: result.currentTag, 84 | switched: result.switched, 85 | previousTag: result.previousTag, 86 | taskCount: result.taskCount, 87 | message: `Successfully switched to tag "${result.currentTag}"` 88 | } 89 | }; 90 | } catch (error) { 91 | // Make sure to restore normal logging even if there's an error 92 | disableSilentMode(); 93 | 94 | log.error(`Error in useTagDirect: ${error.message}`); 95 | return { 96 | success: false, 97 | error: { 98 | code: error.code || 'USE_TAG_ERROR', 99 | message: error.message 100 | } 101 | }; 102 | } 103 | } 104 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/add-dependency.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/add-dependency.js 3 | * Tool for adding a dependency to a task 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | handleApiResult, 9 | createErrorResponse, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { addDependencyDirect } from '../core/task-master-core.js'; 13 | import { findTasksPath } from '../core/utils/path-utils.js'; 14 | import { resolveTag } from '../../../scripts/modules/utils.js'; 15 | 16 | /** 17 | * Register the addDependency tool with the MCP server 18 | * @param {Object} server - FastMCP server instance 19 | */ 20 | export function registerAddDependencyTool(server) { 21 | server.addTool({ 22 | name: 'add_dependency', 23 | description: 'Add a dependency relationship between two tasks', 24 | parameters: z.object({ 25 | id: z.string().describe('ID of task that will depend on another task'), 26 | dependsOn: z 27 | .string() 28 | .describe('ID of task that will become a dependency'), 29 | file: z 30 | .string() 31 | .optional() 32 | .describe( 33 | 'Absolute path to the tasks file (default: tasks/tasks.json)' 34 | ), 35 | projectRoot: z 36 | .string() 37 | .describe('The directory of the project. Must be an absolute path.'), 38 | tag: z.string().optional().describe('Tag context to operate on') 39 | }), 40 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 41 | try { 42 | log.info( 43 | `Adding dependency for task ${args.id} to depend on ${args.dependsOn}` 44 | ); 45 | const resolvedTag = resolveTag({ 46 | projectRoot: args.projectRoot, 47 | tag: args.tag 48 | }); 49 | let tasksJsonPath; 50 | try { 51 | tasksJsonPath = findTasksPath( 52 | { projectRoot: args.projectRoot, file: args.file }, 53 | log 54 | ); 55 | } catch (error) { 56 | log.error(`Error finding tasks.json: ${error.message}`); 57 | return createErrorResponse( 58 | `Failed to find tasks.json: ${error.message}` 59 | ); 60 | } 61 | 62 | // Call the direct function with the resolved path 63 | const result = await addDependencyDirect( 64 | { 65 | // Pass the explicitly resolved path 66 | tasksJsonPath: tasksJsonPath, 67 | // Pass other relevant args 68 | id: args.id, 69 | dependsOn: args.dependsOn, 70 | projectRoot: args.projectRoot, 71 | tag: resolvedTag 72 | }, 73 | log 74 | // Remove context object 75 | ); 76 | 77 | // Log result 78 | if (result.success) { 79 | log.info(`Successfully added dependency: ${result.data.message}`); 80 | } else { 81 | log.error(`Failed to add dependency: ${result.error.message}`); 82 | } 83 | 84 | // Use handleApiResult to format the response 85 | return handleApiResult( 86 | result, 87 | log, 88 | 'Error adding dependency', 89 | undefined, 90 | args.projectRoot 91 | ); 92 | } catch (error) { 93 | log.error(`Error in addDependency tool: ${error.message}`); 94 | return createErrorResponse(error.message); 95 | } 96 | }) 97 | }); 98 | } 99 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/clear-subtasks.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/clear-subtasks.js 3 | * Tool for clearing subtasks from parent tasks 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | handleApiResult, 9 | createErrorResponse, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { clearSubtasksDirect } from '../core/task-master-core.js'; 13 | import { findTasksPath } from '../core/utils/path-utils.js'; 14 | import { resolveTag } from '../../../scripts/modules/utils.js'; 15 | 16 | /** 17 | * Register the clearSubtasks tool with the MCP server 18 | * @param {Object} server - FastMCP server instance 19 | */ 20 | export function registerClearSubtasksTool(server) { 21 | server.addTool({ 22 | name: 'clear_subtasks', 23 | description: 'Clear subtasks from specified tasks', 24 | parameters: z 25 | .object({ 26 | id: z 27 | .string() 28 | .optional() 29 | .describe('Task IDs (comma-separated) to clear subtasks from'), 30 | all: z.boolean().optional().describe('Clear subtasks from all tasks'), 31 | file: z 32 | .string() 33 | .optional() 34 | .describe( 35 | 'Absolute path to the tasks file (default: tasks/tasks.json)' 36 | ), 37 | projectRoot: z 38 | .string() 39 | .describe('The directory of the project. Must be an absolute path.'), 40 | tag: z.string().optional().describe('Tag context to operate on') 41 | }) 42 | .refine((data) => data.id || data.all, { 43 | message: "Either 'id' or 'all' parameter must be provided", 44 | path: ['id', 'all'] 45 | }), 46 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 47 | try { 48 | log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`); 49 | 50 | const resolvedTag = resolveTag({ 51 | projectRoot: args.projectRoot, 52 | tag: args.tag 53 | }); 54 | 55 | // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) 56 | let tasksJsonPath; 57 | try { 58 | tasksJsonPath = findTasksPath( 59 | { projectRoot: args.projectRoot, file: args.file }, 60 | log 61 | ); 62 | } catch (error) { 63 | log.error(`Error finding tasks.json: ${error.message}`); 64 | return createErrorResponse( 65 | `Failed to find tasks.json: ${error.message}` 66 | ); 67 | } 68 | 69 | const result = await clearSubtasksDirect( 70 | { 71 | tasksJsonPath: tasksJsonPath, 72 | id: args.id, 73 | all: args.all, 74 | 75 | projectRoot: args.projectRoot, 76 | tag: resolvedTag 77 | }, 78 | log, 79 | { session } 80 | ); 81 | 82 | if (result.success) { 83 | log.info(`Subtasks cleared successfully: ${result.data.message}`); 84 | } else { 85 | log.error(`Failed to clear subtasks: ${result.error.message}`); 86 | } 87 | 88 | return handleApiResult( 89 | result, 90 | log, 91 | 'Error clearing subtasks', 92 | undefined, 93 | args.projectRoot 94 | ); 95 | } catch (error) { 96 | log.error(`Error in clearSubtasks tool: ${error.message}`); 97 | return createErrorResponse(error.message); 98 | } 99 | }) 100 | }); 101 | } 102 | ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/utils-tag-aware-paths.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Test for getTagAwareFilePath utility function 3 | * Tests the fix for Issue #850 4 | */ 5 | 6 | import { getTagAwareFilePath } from '../../../../scripts/modules/utils.js'; 7 | import path from 'path'; 8 | 9 | describe('getTagAwareFilePath utility function', () => { 10 | const projectRoot = '/test/project'; 11 | const basePath = '.taskmaster/reports/task-complexity-report.json'; 12 | 13 | it('should return base path for master tag', () => { 14 | const result = getTagAwareFilePath(basePath, 'master', projectRoot); 15 | const expected = path.join(projectRoot, basePath); 16 | expect(result).toBe(expected); 17 | }); 18 | 19 | it('should return base path for null tag', () => { 20 | const result = getTagAwareFilePath(basePath, null, projectRoot); 21 | const expected = path.join(projectRoot, basePath); 22 | expect(result).toBe(expected); 23 | }); 24 | 25 | it('should return base path for undefined tag', () => { 26 | const result = getTagAwareFilePath(basePath, undefined, projectRoot); 27 | const expected = path.join(projectRoot, basePath); 28 | expect(result).toBe(expected); 29 | }); 30 | 31 | it('should return tag-specific path for non-master tag', () => { 32 | const tag = 'feature-branch'; 33 | const result = getTagAwareFilePath(basePath, tag, projectRoot); 34 | const expected = path.join( 35 | projectRoot, 36 | '.taskmaster/reports/task-complexity-report_feature-branch.json' 37 | ); 38 | expect(result).toBe(expected); 39 | }); 40 | 41 | it('should handle different file extensions', () => { 42 | const csvBasePath = '.taskmaster/reports/export.csv'; 43 | const tag = 'dev-branch'; 44 | const result = getTagAwareFilePath(csvBasePath, tag, projectRoot); 45 | const expected = path.join( 46 | projectRoot, 47 | '.taskmaster/reports/export_dev-branch.csv' 48 | ); 49 | expect(result).toBe(expected); 50 | }); 51 | 52 | it('should handle paths without extensions', () => { 53 | const noExtPath = '.taskmaster/reports/summary'; 54 | const tag = 'test-tag'; 55 | const result = getTagAwareFilePath(noExtPath, tag, projectRoot); 56 | // Since there's no extension, it should append the tag 57 | const expected = path.join( 58 | projectRoot, 59 | '.taskmaster/reports/summary_test-tag' 60 | ); 61 | expect(result).toBe(expected); 62 | }); 63 | 64 | it('should use default project root when not provided', () => { 65 | const tag = 'feature-tag'; 66 | const result = getTagAwareFilePath(basePath, tag); 67 | const expected = path.join( 68 | '.', 69 | '.taskmaster/reports/task-complexity-report_feature-tag.json' 70 | ); 71 | expect(result).toBe(expected); 72 | }); 73 | 74 | it('should handle complex tag names with special characters', () => { 75 | const tag = 'feature-user-auth-v2'; 76 | const result = getTagAwareFilePath(basePath, tag, projectRoot); 77 | const expected = path.join( 78 | projectRoot, 79 | '.taskmaster/reports/task-complexity-report_feature-user-auth-v2.json' 80 | ); 81 | expect(result).toBe(expected); 82 | }); 83 | }); 84 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/scope-down.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/scope-down.js 3 | * Tool to scope down task complexity 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | createErrorResponse, 9 | handleApiResult, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { scopeDownDirect } from '../core/task-master-core.js'; 13 | import { findTasksPath } from '../core/utils/path-utils.js'; 14 | import { resolveTag } from '../../../scripts/modules/utils.js'; 15 | 16 | /** 17 | * Register the scopeDown tool with the MCP server 18 | * @param {Object} server - FastMCP server instance 19 | */ 20 | export function registerScopeDownTool(server) { 21 | server.addTool({ 22 | name: 'scope_down_task', 23 | description: 'Decrease the complexity of one or more tasks using AI', 24 | parameters: z.object({ 25 | id: z 26 | .string() 27 | .describe( 28 | 'Comma-separated list of task IDs to scope down (e.g., "1,3,5")' 29 | ), 30 | strength: z 31 | .string() 32 | .optional() 33 | .describe( 34 | 'Strength level: light, regular, or heavy (default: regular)' 35 | ), 36 | prompt: z 37 | .string() 38 | .optional() 39 | .describe('Custom prompt for specific scoping adjustments'), 40 | file: z 41 | .string() 42 | .optional() 43 | .describe('Path to the tasks file (default: tasks/tasks.json)'), 44 | projectRoot: z 45 | .string() 46 | .describe('The directory of the project. Must be an absolute path.'), 47 | tag: z.string().optional().describe('Tag context to operate on'), 48 | research: z 49 | .boolean() 50 | .optional() 51 | .describe('Whether to use research capabilities for scoping') 52 | }), 53 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 54 | try { 55 | log.info(`Starting scope-down with args: ${JSON.stringify(args)}`); 56 | 57 | const resolvedTag = resolveTag({ 58 | projectRoot: args.projectRoot, 59 | tag: args.tag 60 | }); 61 | 62 | // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) 63 | let tasksJsonPath; 64 | try { 65 | tasksJsonPath = findTasksPath( 66 | { projectRoot: args.projectRoot, file: args.file }, 67 | log 68 | ); 69 | } catch (error) { 70 | log.error(`Error finding tasks.json: ${error.message}`); 71 | return createErrorResponse( 72 | `Failed to find tasks.json: ${error.message}` 73 | ); 74 | } 75 | 76 | // Call the direct function 77 | const result = await scopeDownDirect( 78 | { 79 | tasksJsonPath: tasksJsonPath, 80 | id: args.id, 81 | strength: args.strength, 82 | prompt: args.prompt, 83 | research: args.research, 84 | projectRoot: args.projectRoot, 85 | tag: resolvedTag 86 | }, 87 | log, 88 | { session } 89 | ); 90 | 91 | return handleApiResult( 92 | result, 93 | log, 94 | 'Error scoping down task', 95 | undefined, 96 | args.projectRoot 97 | ); 98 | } catch (error) { 99 | log.error(`Error in scope-down tool: ${error.message}`); 100 | return createErrorResponse(error.message); 101 | } 102 | }) 103 | }); 104 | } 105 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/next-task.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/next-task.js 3 | * Tool to find the next task to work on based on dependencies and status 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | createErrorResponse, 9 | handleApiResult, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { nextTaskDirect } from '../core/task-master-core.js'; 13 | import { 14 | resolveTasksPath, 15 | resolveComplexityReportPath 16 | } from '../core/utils/path-utils.js'; 17 | import { resolveTag } from '../../../scripts/modules/utils.js'; 18 | 19 | /** 20 | * Register the nextTask tool with the MCP server 21 | * @param {Object} server - FastMCP server instance 22 | */ 23 | export function registerNextTaskTool(server) { 24 | server.addTool({ 25 | name: 'next_task', 26 | description: 27 | 'Find the next task to work on based on dependencies and status', 28 | parameters: z.object({ 29 | file: z.string().optional().describe('Absolute path to the tasks file'), 30 | complexityReport: z 31 | .string() 32 | .optional() 33 | .describe( 34 | 'Path to the complexity report file (relative to project root or absolute)' 35 | ), 36 | projectRoot: z 37 | .string() 38 | .describe('The directory of the project. Must be an absolute path.'), 39 | tag: z.string().optional().describe('Tag context to operate on') 40 | }), 41 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 42 | try { 43 | log.info(`Finding next task with args: ${JSON.stringify(args)}`); 44 | const resolvedTag = resolveTag({ 45 | projectRoot: args.projectRoot, 46 | tag: args.tag 47 | }); 48 | 49 | // Resolve the path to tasks.json using new path utilities 50 | let tasksJsonPath; 51 | try { 52 | tasksJsonPath = resolveTasksPath(args, session); 53 | } catch (error) { 54 | log.error(`Error finding tasks.json: ${error.message}`); 55 | return createErrorResponse( 56 | `Failed to find tasks.json: ${error.message}` 57 | ); 58 | } 59 | 60 | // Resolve the path to complexity report (optional) 61 | let complexityReportPath; 62 | try { 63 | complexityReportPath = resolveComplexityReportPath( 64 | { ...args, tag: resolvedTag }, 65 | session 66 | ); 67 | } catch (error) { 68 | log.error(`Error finding complexity report: ${error.message}`); 69 | // This is optional, so we don't fail the operation 70 | complexityReportPath = null; 71 | } 72 | 73 | const result = await nextTaskDirect( 74 | { 75 | tasksJsonPath: tasksJsonPath, 76 | reportPath: complexityReportPath, 77 | projectRoot: args.projectRoot, 78 | tag: resolvedTag 79 | }, 80 | log, 81 | { session } 82 | ); 83 | 84 | log.info(`Next task result: ${result.success ? 'found' : 'none'}`); 85 | return handleApiResult( 86 | result, 87 | log, 88 | 'Error finding next task', 89 | undefined, 90 | args.projectRoot 91 | ); 92 | } catch (error) { 93 | log.error(`Error finding next task: ${error.message}`); 94 | return createErrorResponse(error.message); 95 | } 96 | }) 97 | }); 98 | } 99 | ``` -------------------------------------------------------------------------------- /src/utils/asset-resolver.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Asset Resolver Module 3 | * Handles resolving paths to asset files in the package 4 | * 5 | * The public/assets folder is copied to dist/assets during build via tsup's publicDir, 6 | * so we can reliably find it relative to the bundled files. 7 | */ 8 | import fs from 'fs'; 9 | import path from 'path'; 10 | import { fileURLToPath } from 'url'; 11 | 12 | const __filename = fileURLToPath(import.meta.url); 13 | const __dirname = path.dirname(__filename); 14 | 15 | /** 16 | * Get the assets directory path 17 | * When bundled, assets are in dist/assets 18 | * When in development, assets are in public/assets 19 | * @returns {string} Path to the assets directory 20 | */ 21 | export function getAssetsDir() { 22 | // Check multiple possible locations 23 | const possiblePaths = [ 24 | // When running from dist (bundled) - assets are in dist/assets 25 | path.join(__dirname, 'assets'), 26 | path.join(__dirname, '..', 'assets'), 27 | // When running from source in development - now in public/assets 28 | path.join(__dirname, '..', '..', 'public', 'assets'), 29 | // When installed as npm package - assets at package root 30 | path.join(process.cwd(), 'assets'), 31 | // For npx usage - check node_modules 32 | path.join( 33 | process.cwd(), 34 | 'node_modules', 35 | 'task-master-ai', 36 | 'dist', 37 | 'assets' 38 | ), 39 | path.join(process.cwd(), 'node_modules', 'task-master-ai', 'assets') 40 | ]; 41 | 42 | // Find the first existing assets directory 43 | for (const assetPath of possiblePaths) { 44 | if (fs.existsSync(assetPath)) { 45 | // Verify it's actually the assets directory by checking for known files 46 | const testFile = path.join(assetPath, 'rules', 'taskmaster.mdc'); 47 | if (fs.existsSync(testFile)) { 48 | return assetPath; 49 | } 50 | } 51 | } 52 | 53 | // If no assets directory found, throw an error 54 | throw new Error( 55 | 'Assets directory not found. This is likely a packaging issue.' 56 | ); 57 | } 58 | 59 | /** 60 | * Get path to a specific asset file 61 | * @param {string} relativePath - Path relative to assets directory 62 | * @returns {string} Full path to the asset file 63 | */ 64 | export function getAssetPath(relativePath) { 65 | const assetsDir = getAssetsDir(); 66 | return path.join(assetsDir, relativePath); 67 | } 68 | 69 | /** 70 | * Check if an asset file exists 71 | * @param {string} relativePath - Path relative to assets directory 72 | * @returns {boolean} True if the asset exists 73 | */ 74 | export function assetExists(relativePath) { 75 | try { 76 | const assetPath = getAssetPath(relativePath); 77 | return fs.existsSync(assetPath); 78 | } catch (error) { 79 | return false; 80 | } 81 | } 82 | 83 | /** 84 | * Read an asset file 85 | * @param {string} relativePath - Path relative to assets directory 86 | * @param {string} encoding - File encoding (default: 'utf8') 87 | * @returns {string|Buffer} File contents 88 | */ 89 | export function readAsset(relativePath, encoding = 'utf8') { 90 | const assetPath = getAssetPath(relativePath); 91 | return fs.readFileSync(assetPath, encoding); 92 | } 93 | ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/move-task.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | 3 | // --- Mocks --- 4 | // Only mock the specific functions that move-task actually uses 5 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ 6 | readJSON: jest.fn(), 7 | writeJSON: jest.fn(), 8 | log: jest.fn(), 9 | setTasksForTag: jest.fn(), 10 | traverseDependencies: jest.fn(() => []) 11 | })); 12 | 13 | jest.unstable_mockModule( 14 | '../../../../../scripts/modules/task-manager/generate-task-files.js', 15 | () => ({ 16 | default: jest.fn().mockResolvedValue() 17 | }) 18 | ); 19 | 20 | jest.unstable_mockModule( 21 | '../../../../../scripts/modules/task-manager/is-task-dependent.js', 22 | () => ({ 23 | default: jest.fn(() => false) 24 | }) 25 | ); 26 | 27 | jest.unstable_mockModule( 28 | '../../../../../scripts/modules/dependency-manager.js', 29 | () => ({ 30 | findCrossTagDependencies: jest.fn(() => []), 31 | getDependentTaskIds: jest.fn(() => []), 32 | validateSubtaskMove: jest.fn() 33 | }) 34 | ); 35 | 36 | const { readJSON, writeJSON, log } = await import( 37 | '../../../../../scripts/modules/utils.js' 38 | ); 39 | const generateTaskFiles = ( 40 | await import( 41 | '../../../../../scripts/modules/task-manager/generate-task-files.js' 42 | ) 43 | ).default; 44 | 45 | const { default: moveTask } = await import( 46 | '../../../../../scripts/modules/task-manager/move-task.js' 47 | ); 48 | 49 | const sampleTagged = () => ({ 50 | master: { 51 | tasks: [ 52 | { id: 1, title: 'A' }, 53 | { id: 2, title: 'B', subtasks: [{ id: 1, title: 'B.1' }] } 54 | ], 55 | metadata: {} 56 | }, 57 | feature: { 58 | tasks: [{ id: 10, title: 'X' }], 59 | metadata: {} 60 | } 61 | }); 62 | 63 | const clone = () => JSON.parse(JSON.stringify(sampleTagged())); 64 | 65 | describe('moveTask (unit)', () => { 66 | beforeEach(() => { 67 | jest.clearAllMocks(); 68 | readJSON.mockImplementation((path, projectRoot, tag) => { 69 | const data = clone(); 70 | return { ...data[tag], tag, _rawTaggedData: data }; 71 | }); 72 | writeJSON.mockResolvedValue(); 73 | log.mockImplementation(() => {}); 74 | }); 75 | 76 | test('moves task to new ID in same tag', async () => { 77 | await moveTask('tasks.json', '1', '3', false, { tag: 'master' }); 78 | expect(writeJSON).toHaveBeenCalled(); 79 | const written = writeJSON.mock.calls[0][1]; 80 | const ids = written.master.tasks.map((t) => t.id); 81 | expect(ids).toEqual(expect.arrayContaining([2, 3])); 82 | expect(ids).not.toContain(1); 83 | }); 84 | 85 | test('throws when counts of source and dest mismatch', async () => { 86 | await expect( 87 | moveTask('tasks.json', '1,2', '3', {}, { tag: 'master' }) 88 | ).rejects.toThrow(/Number of source IDs/); 89 | }); 90 | 91 | test('batch move calls generateTaskFiles once when flag true', async () => { 92 | await moveTask('tasks.json', '1,2', '3,4', true, { tag: 'master' }); 93 | expect(generateTaskFiles).toHaveBeenCalledTimes(1); 94 | }); 95 | 96 | test('error when tag invalid', async () => { 97 | await expect( 98 | moveTask('tasks.json', '1', '2', false, { tag: 'ghost' }) 99 | ).rejects.toThrow(/tag "ghost" not found/); 100 | }); 101 | }); 102 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/move-task.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Direct function wrapper for moveTask 3 | */ 4 | 5 | import { moveTask } from '../../../../scripts/modules/task-manager.js'; 6 | import { findTasksPath } from '../utils/path-utils.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode 10 | } from '../../../../scripts/modules/utils.js'; 11 | 12 | /** 13 | * Move a task or subtask to a new position 14 | * @param {Object} args - Function arguments 15 | * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file 16 | * @param {string} args.sourceId - ID of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7') 17 | * @param {string} args.destinationId - ID of the destination (e.g., '7' or '7.3' or '7,8,9') 18 | * @param {string} args.file - Alternative path to the tasks.json file 19 | * @param {string} args.projectRoot - Project root directory 20 | * @param {string} args.tag - Tag for the task (optional) 21 | * @param {boolean} args.generateFiles - Whether to regenerate task files after moving (default: true) 22 | * @param {Object} log - Logger object 23 | * @returns {Promise<{success: boolean, data?: Object, error?: Object}>} 24 | */ 25 | export async function moveTaskDirect(args, log, context = {}) { 26 | const { session } = context; 27 | const { projectRoot, tag } = args; 28 | 29 | // Validate required parameters 30 | if (!args.sourceId) { 31 | return { 32 | success: false, 33 | error: { 34 | message: 'Source ID is required', 35 | code: 'MISSING_SOURCE_ID' 36 | } 37 | }; 38 | } 39 | 40 | if (!args.destinationId) { 41 | return { 42 | success: false, 43 | error: { 44 | message: 'Destination ID is required', 45 | code: 'MISSING_DESTINATION_ID' 46 | } 47 | }; 48 | } 49 | 50 | try { 51 | // Find tasks.json path if not provided 52 | let tasksPath = args.tasksJsonPath || args.file; 53 | if (!tasksPath) { 54 | if (!args.projectRoot) { 55 | return { 56 | success: false, 57 | error: { 58 | message: 59 | 'Project root is required if tasksJsonPath is not provided', 60 | code: 'MISSING_PROJECT_ROOT' 61 | } 62 | }; 63 | } 64 | tasksPath = findTasksPath(args, log); 65 | } 66 | 67 | // Enable silent mode to prevent console output during MCP operation 68 | enableSilentMode(); 69 | 70 | // Call the core moveTask function with file generation control 71 | const generateFiles = args.generateFiles !== false; // Default to true 72 | const result = await moveTask( 73 | tasksPath, 74 | args.sourceId, 75 | args.destinationId, 76 | generateFiles, 77 | { 78 | projectRoot, 79 | tag 80 | } 81 | ); 82 | 83 | // Restore console output 84 | disableSilentMode(); 85 | 86 | return { 87 | success: true, 88 | data: { 89 | ...result, 90 | message: `Successfully moved task/subtask ${args.sourceId} to ${args.destinationId}` 91 | } 92 | }; 93 | } catch (error) { 94 | // Restore console output in case of error 95 | disableSilentMode(); 96 | 97 | log.error(`Failed to move task: ${error.message}`); 98 | 99 | return { 100 | success: false, 101 | error: { 102 | message: error.message, 103 | code: 'MOVE_TASK_ERROR' 104 | } 105 | }; 106 | } 107 | } 108 | ``` -------------------------------------------------------------------------------- /.github/workflows/claude-dedupe-issues.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Claude Issue Dedupe 2 | # description: Automatically dedupe GitHub issues using Claude Code 3 | 4 | on: 5 | issues: 6 | types: [opened] 7 | workflow_dispatch: 8 | inputs: 9 | issue_number: 10 | description: "Issue number to process for duplicate detection" 11 | required: true 12 | type: string 13 | 14 | jobs: 15 | claude-dedupe-issues: 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | permissions: 19 | contents: read 20 | issues: write 21 | 22 | steps: 23 | - name: Checkout repository 24 | uses: actions/checkout@v4 25 | 26 | - name: Run Claude Code slash command 27 | uses: anthropics/claude-code-base-action@beta 28 | with: 29 | prompt: "/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}" 30 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 31 | claude_env: | 32 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Log duplicate comment event to Statsig 35 | if: always() 36 | env: 37 | STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }} 38 | run: | 39 | ISSUE_NUMBER=${{ github.event.issue.number || inputs.issue_number }} 40 | REPO=${{ github.repository }} 41 | 42 | if [ -z "$STATSIG_API_KEY" ]; then 43 | echo "STATSIG_API_KEY not found, skipping Statsig logging" 44 | exit 0 45 | fi 46 | 47 | # Prepare the event payload 48 | EVENT_PAYLOAD=$(jq -n \ 49 | --arg issue_number "$ISSUE_NUMBER" \ 50 | --arg repo "$REPO" \ 51 | --arg triggered_by "${{ github.event_name }}" \ 52 | '{ 53 | events: [{ 54 | eventName: "github_duplicate_comment_added", 55 | value: 1, 56 | metadata: { 57 | repository: $repo, 58 | issue_number: ($issue_number | tonumber), 59 | triggered_by: $triggered_by, 60 | workflow_run_id: "${{ github.run_id }}" 61 | }, 62 | time: (now | floor | tostring) 63 | }] 64 | }') 65 | 66 | # Send to Statsig API 67 | echo "Logging duplicate comment event to Statsig for issue #${ISSUE_NUMBER}" 68 | 69 | RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \ 70 | -H "Content-Type: application/json" \ 71 | -H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \ 72 | -d "$EVENT_PAYLOAD") 73 | 74 | HTTP_CODE=$(echo "$RESPONSE" | tail -n1) 75 | BODY=$(echo "$RESPONSE" | head -n-1) 76 | 77 | if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then 78 | echo "Successfully logged duplicate comment event for issue #${ISSUE_NUMBER}" 79 | else 80 | echo "Failed to log duplicate comment event for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}" 81 | fi 82 | ``` -------------------------------------------------------------------------------- /tests/unit/ai-providers/mcp-components.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tests/unit/ai-providers/mcp-components.test.js 3 | * Unit tests for MCP AI SDK custom components 4 | */ 5 | 6 | import { jest } from '@jest/globals'; 7 | 8 | describe('MCP Custom SDK Components', () => { 9 | describe('Message Converter', () => { 10 | let messageConverter; 11 | 12 | beforeAll(async () => { 13 | const module = await import( 14 | '../../../mcp-server/src/custom-sdk/message-converter.js' 15 | ); 16 | messageConverter = module; 17 | }); 18 | 19 | describe('convertToMCPFormat', () => { 20 | it('should convert AI SDK messages to MCP format', () => { 21 | const input = [ 22 | { role: 'system', content: 'You are a helpful assistant.' }, 23 | { role: 'user', content: 'Hello!' } 24 | ]; 25 | 26 | const result = messageConverter.convertToMCPFormat(input); 27 | 28 | expect(result).toBeDefined(); 29 | expect(result.messages).toBeDefined(); 30 | expect(Array.isArray(result.messages)).toBe(true); 31 | expect(result.systemPrompt).toBe('You are a helpful assistant.'); 32 | expect(result.messages).toHaveLength(1); 33 | expect(result.messages[0].role).toBe('user'); 34 | expect(result.messages[0].content.text).toBe('Hello!'); 35 | }); 36 | }); 37 | 38 | describe('convertFromMCPFormat', () => { 39 | it('should convert MCP response to AI SDK format', () => { 40 | const input = { 41 | content: 'Hello! How can I help you?', 42 | usage: { inputTokens: 10, outputTokens: 8 } 43 | }; 44 | 45 | const result = messageConverter.convertFromMCPFormat(input); 46 | 47 | expect(result).toBeDefined(); 48 | expect(result.text).toBe('Hello! How can I help you?'); 49 | expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 8 }); 50 | expect(result.finishReason).toBe('stop'); 51 | expect(result.warnings).toBeDefined(); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('Language Model', () => { 57 | let languageModel; 58 | 59 | beforeAll(async () => { 60 | const module = await import( 61 | '../../../mcp-server/src/custom-sdk/language-model.js' 62 | ); 63 | languageModel = module; 64 | }); 65 | 66 | it('should export MCPLanguageModel class', () => { 67 | expect(languageModel.MCPLanguageModel).toBeDefined(); 68 | expect(typeof languageModel.MCPLanguageModel).toBe('function'); 69 | }); 70 | }); 71 | 72 | describe('Error Handling', () => { 73 | let errors; 74 | 75 | beforeAll(async () => { 76 | const module = await import( 77 | '../../../mcp-server/src/custom-sdk/errors.js' 78 | ); 79 | errors = module; 80 | }); 81 | 82 | it('should export error classes', () => { 83 | expect(errors.MCPError).toBeDefined(); 84 | expect(typeof errors.MCPError).toBe('function'); 85 | }); 86 | }); 87 | 88 | describe('Index Module', () => { 89 | let index; 90 | 91 | beforeAll(async () => { 92 | const module = await import( 93 | '../../../mcp-server/src/custom-sdk/index.js' 94 | ); 95 | index = module; 96 | }); 97 | 98 | it('should export createMCP function', () => { 99 | expect(index.createMCP).toBeDefined(); 100 | expect(typeof index.createMCP).toBe('function'); 101 | }); 102 | }); 103 | }); 104 | ``` -------------------------------------------------------------------------------- /tests/unit/mcp-providers/mcp-components.test.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tests/unit/mcp-providers/mcp-components.test.js 3 | * Unit tests for MCP AI SDK custom components 4 | */ 5 | 6 | import { jest } from '@jest/globals'; 7 | 8 | describe('MCP Custom SDK Components', () => { 9 | describe('Message Converter', () => { 10 | let messageConverter; 11 | 12 | beforeAll(async () => { 13 | const module = await import( 14 | '../../../mcp-server/src/custom-sdk/message-converter.js' 15 | ); 16 | messageConverter = module; 17 | }); 18 | 19 | describe('convertToMCPFormat', () => { 20 | it('should convert AI SDK messages to MCP format', () => { 21 | const input = [ 22 | { role: 'system', content: 'You are a helpful assistant.' }, 23 | { role: 'user', content: 'Hello!' } 24 | ]; 25 | 26 | const result = messageConverter.convertToMCPFormat(input); 27 | 28 | expect(result).toBeDefined(); 29 | expect(result.messages).toBeDefined(); 30 | expect(Array.isArray(result.messages)).toBe(true); 31 | expect(result.systemPrompt).toBe('You are a helpful assistant.'); 32 | expect(result.messages).toHaveLength(1); 33 | expect(result.messages[0].role).toBe('user'); 34 | expect(result.messages[0].content.text).toBe('Hello!'); 35 | }); 36 | }); 37 | 38 | describe('convertFromMCPFormat', () => { 39 | it('should convert MCP response to AI SDK format', () => { 40 | const input = { 41 | content: 'Hello! How can I help you?', 42 | usage: { inputTokens: 10, outputTokens: 8 } 43 | }; 44 | 45 | const result = messageConverter.convertFromMCPFormat(input); 46 | 47 | expect(result).toBeDefined(); 48 | expect(result.text).toBe('Hello! How can I help you?'); 49 | expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 8 }); 50 | expect(result.finishReason).toBe('stop'); 51 | expect(result.warnings).toBeDefined(); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('Language Model', () => { 57 | let languageModel; 58 | 59 | beforeAll(async () => { 60 | const module = await import( 61 | '../../../mcp-server/src/custom-sdk/language-model.js' 62 | ); 63 | languageModel = module; 64 | }); 65 | 66 | it('should export MCPLanguageModel class', () => { 67 | expect(languageModel.MCPLanguageModel).toBeDefined(); 68 | expect(typeof languageModel.MCPLanguageModel).toBe('function'); 69 | }); 70 | }); 71 | 72 | describe('Error Handling', () => { 73 | let errors; 74 | 75 | beforeAll(async () => { 76 | const module = await import( 77 | '../../../mcp-server/src/custom-sdk/errors.js' 78 | ); 79 | errors = module; 80 | }); 81 | 82 | it('should export error classes', () => { 83 | expect(errors.MCPError).toBeDefined(); 84 | expect(typeof errors.MCPError).toBe('function'); 85 | }); 86 | }); 87 | 88 | describe('Index Module', () => { 89 | let index; 90 | 91 | beforeAll(async () => { 92 | const module = await import( 93 | '../../../mcp-server/src/custom-sdk/index.js' 94 | ); 95 | index = module; 96 | }); 97 | 98 | it('should export createMCP function', () => { 99 | expect(index.createMCP).toBeDefined(); 100 | expect(typeof index.createMCP).toBe('function'); 101 | }); 102 | }); 103 | }); 104 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/remove-subtask.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/remove-subtask.js 3 | * Tool for removing subtasks from parent tasks 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | handleApiResult, 9 | createErrorResponse, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { removeSubtaskDirect } from '../core/task-master-core.js'; 13 | import { findTasksPath } from '../core/utils/path-utils.js'; 14 | import { resolveTag } from '../../../scripts/modules/utils.js'; 15 | 16 | /** 17 | * Register the removeSubtask tool with the MCP server 18 | * @param {Object} server - FastMCP server instance 19 | */ 20 | export function registerRemoveSubtaskTool(server) { 21 | server.addTool({ 22 | name: 'remove_subtask', 23 | description: 'Remove a subtask from its parent task', 24 | parameters: z.object({ 25 | id: z 26 | .string() 27 | .describe( 28 | "Subtask ID to remove in format 'parentId.subtaskId' (required)" 29 | ), 30 | convert: z 31 | .boolean() 32 | .optional() 33 | .describe( 34 | 'Convert the subtask to a standalone task instead of deleting it' 35 | ), 36 | file: z 37 | .string() 38 | .optional() 39 | .describe( 40 | 'Absolute path to the tasks file (default: tasks/tasks.json)' 41 | ), 42 | skipGenerate: z 43 | .boolean() 44 | .optional() 45 | .describe('Skip regenerating task files'), 46 | projectRoot: z 47 | .string() 48 | .describe('The directory of the project. Must be an absolute path.'), 49 | tag: z.string().optional().describe('Tag context to operate on') 50 | }), 51 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 52 | try { 53 | const resolvedTag = resolveTag({ 54 | projectRoot: args.projectRoot, 55 | tag: args.tag 56 | }); 57 | log.info(`Removing subtask with args: ${JSON.stringify(args)}`); 58 | 59 | // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) 60 | let tasksJsonPath; 61 | try { 62 | tasksJsonPath = findTasksPath( 63 | { projectRoot: args.projectRoot, file: args.file }, 64 | log 65 | ); 66 | } catch (error) { 67 | log.error(`Error finding tasks.json: ${error.message}`); 68 | return createErrorResponse( 69 | `Failed to find tasks.json: ${error.message}` 70 | ); 71 | } 72 | 73 | const result = await removeSubtaskDirect( 74 | { 75 | tasksJsonPath: tasksJsonPath, 76 | id: args.id, 77 | convert: args.convert, 78 | skipGenerate: args.skipGenerate, 79 | projectRoot: args.projectRoot, 80 | tag: resolvedTag 81 | }, 82 | log, 83 | { session } 84 | ); 85 | 86 | if (result.success) { 87 | log.info(`Subtask removed successfully: ${result.data.message}`); 88 | } else { 89 | log.error(`Failed to remove subtask: ${result.error.message}`); 90 | } 91 | 92 | return handleApiResult( 93 | result, 94 | log, 95 | 'Error removing subtask', 96 | undefined, 97 | args.projectRoot 98 | ); 99 | } catch (error) { 100 | log.error(`Error in removeSubtask tool: ${error.message}`); 101 | return createErrorResponse(error.message); 102 | } 103 | }) 104 | }); 105 | } 106 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/models.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * models.js 3 | * MCP tool for managing AI model configurations 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | handleApiResult, 9 | createErrorResponse, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { modelsDirect } from '../core/task-master-core.js'; 13 | 14 | /** 15 | * Register the models tool with the MCP server 16 | * @param {Object} server - FastMCP server instance 17 | */ 18 | export function registerModelsTool(server) { 19 | server.addTool({ 20 | name: 'models', 21 | description: 22 | 'Get information about available AI models or set model configurations. Run without arguments to get the current model configuration and API key status for the selected model providers.', 23 | parameters: z.object({ 24 | setMain: z 25 | .string() 26 | .optional() 27 | .describe( 28 | 'Set the primary model for task generation/updates. Model provider API key is required in the MCP config ENV.' 29 | ), 30 | setResearch: z 31 | .string() 32 | .optional() 33 | .describe( 34 | 'Set the model for research-backed operations. Model provider API key is required in the MCP config ENV.' 35 | ), 36 | setFallback: z 37 | .string() 38 | .optional() 39 | .describe( 40 | 'Set the model to use if the primary fails. Model provider API key is required in the MCP config ENV.' 41 | ), 42 | listAvailableModels: z 43 | .boolean() 44 | .optional() 45 | .describe( 46 | 'List all available models not currently in use. Input/output costs values are in dollars (3 is $3.00).' 47 | ), 48 | projectRoot: z 49 | .string() 50 | .describe('The directory of the project. Must be an absolute path.'), 51 | openrouter: z 52 | .boolean() 53 | .optional() 54 | .describe('Indicates the set model ID is a custom OpenRouter model.'), 55 | ollama: z 56 | .boolean() 57 | .optional() 58 | .describe('Indicates the set model ID is a custom Ollama model.'), 59 | bedrock: z 60 | .boolean() 61 | .optional() 62 | .describe('Indicates the set model ID is a custom AWS Bedrock model.'), 63 | azure: z 64 | .boolean() 65 | .optional() 66 | .describe('Indicates the set model ID is a custom Azure OpenAI model.'), 67 | vertex: z 68 | .boolean() 69 | .optional() 70 | .describe( 71 | 'Indicates the set model ID is a custom Google Vertex AI model.' 72 | ) 73 | }), 74 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 75 | try { 76 | log.info(`Starting models tool with args: ${JSON.stringify(args)}`); 77 | 78 | // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) 79 | const result = await modelsDirect( 80 | { ...args, projectRoot: args.projectRoot }, 81 | log, 82 | { session } 83 | ); 84 | 85 | return handleApiResult( 86 | result, 87 | log, 88 | 'Error managing models', 89 | undefined, 90 | args.projectRoot 91 | ); 92 | } catch (error) { 93 | log.error(`Error in models tool: ${error.message}`); 94 | return createErrorResponse(error.message); 95 | } 96 | }) 97 | }); 98 | } 99 | ``` -------------------------------------------------------------------------------- /packages/tm-core/src/config/services/config-merger.service.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Configuration Merger Service 3 | * Responsible for merging configurations from multiple sources with precedence 4 | */ 5 | 6 | import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; 7 | 8 | /** 9 | * Configuration source with precedence 10 | */ 11 | export interface ConfigSource { 12 | /** Source name for debugging */ 13 | name: string; 14 | /** Configuration data from this source */ 15 | config: PartialConfiguration; 16 | /** Precedence level (higher = more important) */ 17 | precedence: number; 18 | } 19 | 20 | /** 21 | * Configuration precedence levels (higher number = higher priority) 22 | */ 23 | export const CONFIG_PRECEDENCE = { 24 | DEFAULTS: 0, 25 | GLOBAL: 1, // Reserved for future implementation 26 | LOCAL: 2, 27 | ENVIRONMENT: 3 28 | } as const; 29 | 30 | /** 31 | * ConfigMerger handles merging configurations with precedence rules 32 | * Single responsibility: Configuration merging logic 33 | */ 34 | export class ConfigMerger { 35 | private configSources: ConfigSource[] = []; 36 | 37 | /** 38 | * Add a configuration source 39 | */ 40 | addSource(source: ConfigSource): void { 41 | this.configSources.push(source); 42 | } 43 | 44 | /** 45 | * Clear all configuration sources 46 | */ 47 | clearSources(): void { 48 | this.configSources = []; 49 | } 50 | 51 | /** 52 | * Merge all configuration sources based on precedence 53 | */ 54 | merge(): PartialConfiguration { 55 | // Sort sources by precedence (lowest first) 56 | const sortedSources = [...this.configSources].sort( 57 | (a, b) => a.precedence - b.precedence 58 | ); 59 | 60 | // Merge from lowest to highest precedence 61 | let merged: PartialConfiguration = {}; 62 | for (const source of sortedSources) { 63 | merged = this.deepMerge(merged, source.config); 64 | } 65 | 66 | return merged; 67 | } 68 | 69 | /** 70 | * Deep merge two configuration objects 71 | * Higher precedence values override lower ones 72 | */ 73 | private deepMerge(target: any, source: any): any { 74 | if (!source) return target; 75 | if (!target) return source; 76 | 77 | const result = { ...target }; 78 | 79 | for (const key in source) { 80 | if (source[key] === null || source[key] === undefined) { 81 | continue; 82 | } 83 | 84 | if (typeof source[key] === 'object' && !Array.isArray(source[key])) { 85 | result[key] = this.deepMerge(result[key] || {}, source[key]); 86 | } else { 87 | result[key] = source[key]; 88 | } 89 | } 90 | 91 | return result; 92 | } 93 | 94 | /** 95 | * Get configuration sources for debugging 96 | */ 97 | getSources(): ConfigSource[] { 98 | return [...this.configSources].sort((a, b) => b.precedence - a.precedence); 99 | } 100 | 101 | /** 102 | * Check if a source exists 103 | */ 104 | hasSource(name: string): boolean { 105 | return this.configSources.some((source) => source.name === name); 106 | } 107 | 108 | /** 109 | * Remove a source by name 110 | */ 111 | removeSource(name: string): boolean { 112 | const initialLength = this.configSources.length; 113 | this.configSources = this.configSources.filter( 114 | (source) => source.name !== name 115 | ); 116 | return this.configSources.length < initialLength; 117 | } 118 | } 119 | ``` -------------------------------------------------------------------------------- /.claude/commands/tm/analyze-complexity/analyze-complexity.md: -------------------------------------------------------------------------------- ```markdown 1 | Analyze task complexity and generate expansion recommendations. 2 | 3 | Arguments: $ARGUMENTS 4 | 5 | Perform deep analysis of task complexity across the project. 6 | 7 | ## Complexity Analysis 8 | 9 | Uses AI to analyze tasks and recommend which ones need breakdown. 10 | 11 | ## Execution Options 12 | 13 | ```bash 14 | task-master analyze-complexity [--research] [--threshold=5] 15 | ``` 16 | 17 | ## Analysis Parameters 18 | 19 | - `--research` → Use research AI for deeper analysis 20 | - `--threshold=5` → Only flag tasks above complexity 5 21 | - Default: Analyze all pending tasks 22 | 23 | ## Analysis Process 24 | 25 | ### 1. **Task Evaluation** 26 | For each task, AI evaluates: 27 | - Technical complexity 28 | - Time requirements 29 | - Dependency complexity 30 | - Risk factors 31 | - Knowledge requirements 32 | 33 | ### 2. **Complexity Scoring** 34 | Assigns score 1-10 based on: 35 | - Implementation difficulty 36 | - Integration challenges 37 | - Testing requirements 38 | - Unknown factors 39 | - Technical debt risk 40 | 41 | ### 3. **Recommendations** 42 | For complex tasks: 43 | - Suggest expansion approach 44 | - Recommend subtask breakdown 45 | - Identify risk areas 46 | - Propose mitigation strategies 47 | 48 | ## Smart Analysis Features 49 | 50 | 1. **Pattern Recognition** 51 | - Similar task comparisons 52 | - Historical complexity accuracy 53 | - Team velocity consideration 54 | - Technology stack factors 55 | 56 | 2. **Contextual Factors** 57 | - Team expertise 58 | - Available resources 59 | - Timeline constraints 60 | - Business criticality 61 | 62 | 3. **Risk Assessment** 63 | - Technical risks 64 | - Timeline risks 65 | - Dependency risks 66 | - Knowledge gaps 67 | 68 | ## Output Format 69 | 70 | ``` 71 | Task Complexity Analysis Report 72 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 73 | 74 | High Complexity Tasks (>7): 75 | 📍 #5 "Implement real-time sync" - Score: 9/10 76 | Factors: WebSocket complexity, state management, conflict resolution 77 | Recommendation: Expand into 5-7 subtasks 78 | Risks: Performance, data consistency 79 | 80 | 📍 #12 "Migrate database schema" - Score: 8/10 81 | Factors: Data migration, zero downtime, rollback strategy 82 | Recommendation: Expand into 4-5 subtasks 83 | Risks: Data loss, downtime 84 | 85 | Medium Complexity Tasks (5-7): 86 | 📍 #23 "Add export functionality" - Score: 6/10 87 | Consider expansion if timeline tight 88 | 89 | Low Complexity Tasks (<5): 90 | ✅ 15 tasks - No expansion needed 91 | 92 | Summary: 93 | - Expand immediately: 2 tasks 94 | - Consider expanding: 5 tasks 95 | - Keep as-is: 15 tasks 96 | ``` 97 | 98 | ## Actionable Output 99 | 100 | For each high-complexity task: 101 | 1. Complexity score with reasoning 102 | 2. Specific expansion suggestions 103 | 3. Risk mitigation approaches 104 | 4. Recommended subtask structure 105 | 106 | ## Integration 107 | 108 | Results are: 109 | - Saved to `.taskmaster/reports/complexity-analysis.md` 110 | - Used by expand command 111 | - Inform sprint planning 112 | - Guide resource allocation 113 | 114 | ## Next Steps 115 | 116 | After analysis: 117 | ``` 118 | /project:tm/expand 5 # Expand specific task 119 | /project:tm/expand/all # Expand all recommended 120 | /project:tm/complexity-report # View detailed report 121 | ``` ``` -------------------------------------------------------------------------------- /assets/claude/commands/tm/analyze-complexity/analyze-complexity.md: -------------------------------------------------------------------------------- ```markdown 1 | Analyze task complexity and generate expansion recommendations. 2 | 3 | Arguments: $ARGUMENTS 4 | 5 | Perform deep analysis of task complexity across the project. 6 | 7 | ## Complexity Analysis 8 | 9 | Uses AI to analyze tasks and recommend which ones need breakdown. 10 | 11 | ## Execution Options 12 | 13 | ```bash 14 | task-master analyze-complexity [--research] [--threshold=5] 15 | ``` 16 | 17 | ## Analysis Parameters 18 | 19 | - `--research` → Use research AI for deeper analysis 20 | - `--threshold=5` → Only flag tasks above complexity 5 21 | - Default: Analyze all pending tasks 22 | 23 | ## Analysis Process 24 | 25 | ### 1. **Task Evaluation** 26 | For each task, AI evaluates: 27 | - Technical complexity 28 | - Time requirements 29 | - Dependency complexity 30 | - Risk factors 31 | - Knowledge requirements 32 | 33 | ### 2. **Complexity Scoring** 34 | Assigns score 1-10 based on: 35 | - Implementation difficulty 36 | - Integration challenges 37 | - Testing requirements 38 | - Unknown factors 39 | - Technical debt risk 40 | 41 | ### 3. **Recommendations** 42 | For complex tasks: 43 | - Suggest expansion approach 44 | - Recommend subtask breakdown 45 | - Identify risk areas 46 | - Propose mitigation strategies 47 | 48 | ## Smart Analysis Features 49 | 50 | 1. **Pattern Recognition** 51 | - Similar task comparisons 52 | - Historical complexity accuracy 53 | - Team velocity consideration 54 | - Technology stack factors 55 | 56 | 2. **Contextual Factors** 57 | - Team expertise 58 | - Available resources 59 | - Timeline constraints 60 | - Business criticality 61 | 62 | 3. **Risk Assessment** 63 | - Technical risks 64 | - Timeline risks 65 | - Dependency risks 66 | - Knowledge gaps 67 | 68 | ## Output Format 69 | 70 | ``` 71 | Task Complexity Analysis Report 72 | ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 73 | 74 | High Complexity Tasks (>7): 75 | 📍 #5 "Implement real-time sync" - Score: 9/10 76 | Factors: WebSocket complexity, state management, conflict resolution 77 | Recommendation: Expand into 5-7 subtasks 78 | Risks: Performance, data consistency 79 | 80 | 📍 #12 "Migrate database schema" - Score: 8/10 81 | Factors: Data migration, zero downtime, rollback strategy 82 | Recommendation: Expand into 4-5 subtasks 83 | Risks: Data loss, downtime 84 | 85 | Medium Complexity Tasks (5-7): 86 | 📍 #23 "Add export functionality" - Score: 6/10 87 | Consider expansion if timeline tight 88 | 89 | Low Complexity Tasks (<5): 90 | ✅ 15 tasks - No expansion needed 91 | 92 | Summary: 93 | - Expand immediately: 2 tasks 94 | - Consider expanding: 5 tasks 95 | - Keep as-is: 15 tasks 96 | ``` 97 | 98 | ## Actionable Output 99 | 100 | For each high-complexity task: 101 | 1. Complexity score with reasoning 102 | 2. Specific expansion suggestions 103 | 3. Risk mitigation approaches 104 | 4. Recommended subtask structure 105 | 106 | ## Integration 107 | 108 | Results are: 109 | - Saved to `.taskmaster/reports/complexity-analysis.md` 110 | - Used by expand command 111 | - Inform sprint planning 112 | - Guide resource allocation 113 | 114 | ## Next Steps 115 | 116 | After analysis: 117 | ``` 118 | /project:tm/expand 5 # Expand specific task 119 | /project:tm/expand/all # Expand all recommended 120 | /project:tm/complexity-report # View detailed report 121 | ``` ``` -------------------------------------------------------------------------------- /tests/unit/profiles/cline-integration.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import os from 'os'; 5 | 6 | // Mock external modules 7 | jest.mock('child_process', () => ({ 8 | execSync: jest.fn() 9 | })); 10 | 11 | // Mock console methods 12 | jest.mock('console', () => ({ 13 | log: jest.fn(), 14 | info: jest.fn(), 15 | warn: jest.fn(), 16 | error: jest.fn(), 17 | clear: jest.fn() 18 | })); 19 | 20 | describe('Cline Integration', () => { 21 | let tempDir; 22 | 23 | beforeEach(() => { 24 | jest.clearAllMocks(); 25 | 26 | // Create a temporary directory for testing 27 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); 28 | 29 | // Spy on fs methods 30 | jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); 31 | jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { 32 | if (filePath.toString().includes('.clinerules')) { 33 | return 'Existing cline rules content'; 34 | } 35 | return '{}'; 36 | }); 37 | jest.spyOn(fs, 'existsSync').mockImplementation(() => false); 38 | jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); 39 | }); 40 | 41 | afterEach(() => { 42 | // Clean up the temporary directory 43 | try { 44 | fs.rmSync(tempDir, { recursive: true, force: true }); 45 | } catch (err) { 46 | console.error(`Error cleaning up: ${err.message}`); 47 | } 48 | }); 49 | 50 | // Test function that simulates the createProjectStructure behavior for Cline files 51 | function mockCreateClineStructure() { 52 | // Create main .clinerules directory 53 | fs.mkdirSync(path.join(tempDir, '.clinerules'), { recursive: true }); 54 | 55 | // Create rule files 56 | const ruleFiles = [ 57 | 'dev_workflow.md', 58 | 'taskmaster.md', 59 | 'architecture.md', 60 | 'commands.md', 61 | 'dependencies.md' 62 | ]; 63 | 64 | for (const ruleFile of ruleFiles) { 65 | fs.writeFileSync( 66 | path.join(tempDir, '.clinerules', ruleFile), 67 | `Content for ${ruleFile}` 68 | ); 69 | } 70 | } 71 | 72 | test('creates all required .clinerules directories', () => { 73 | // Act 74 | mockCreateClineStructure(); 75 | 76 | // Assert 77 | expect(fs.mkdirSync).toHaveBeenCalledWith( 78 | path.join(tempDir, '.clinerules'), 79 | { recursive: true } 80 | ); 81 | }); 82 | 83 | test('creates rule files for Cline', () => { 84 | // Act 85 | mockCreateClineStructure(); 86 | 87 | // Assert - check rule files are created 88 | expect(fs.writeFileSync).toHaveBeenCalledWith( 89 | path.join(tempDir, '.clinerules', 'dev_workflow.md'), 90 | expect.any(String) 91 | ); 92 | expect(fs.writeFileSync).toHaveBeenCalledWith( 93 | path.join(tempDir, '.clinerules', 'taskmaster.md'), 94 | expect.any(String) 95 | ); 96 | expect(fs.writeFileSync).toHaveBeenCalledWith( 97 | path.join(tempDir, '.clinerules', 'architecture.md'), 98 | expect.any(String) 99 | ); 100 | }); 101 | 102 | test('does not create MCP configuration files', () => { 103 | // Act 104 | mockCreateClineStructure(); 105 | 106 | // Assert - Cline doesn't use MCP configuration 107 | expect(fs.writeFileSync).not.toHaveBeenCalledWith( 108 | path.join(tempDir, '.clinerules', 'mcp.json'), 109 | expect.any(String) 110 | ); 111 | }); 112 | }); 113 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/__tests__/context-manager.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import { ContextManager } from '../context-manager.js'; 3 | 4 | describe('ContextManager', () => { 5 | let contextManager; 6 | 7 | beforeEach(() => { 8 | contextManager = new ContextManager({ 9 | maxCacheSize: 10, 10 | ttl: 1000, // 1 second for testing 11 | maxContextSize: 1000 12 | }); 13 | }); 14 | 15 | describe('getContext', () => { 16 | it('should create a new context when not in cache', async () => { 17 | const context = await contextManager.getContext('test-id', { 18 | test: true 19 | }); 20 | expect(context.id).toBe('test-id'); 21 | expect(context.metadata.test).toBe(true); 22 | expect(contextManager.stats.misses).toBe(1); 23 | expect(contextManager.stats.hits).toBe(0); 24 | }); 25 | 26 | it('should return cached context when available', async () => { 27 | // First call creates the context 28 | await contextManager.getContext('test-id', { test: true }); 29 | 30 | // Second call should hit cache 31 | const context = await contextManager.getContext('test-id', { 32 | test: true 33 | }); 34 | expect(context.id).toBe('test-id'); 35 | expect(context.metadata.test).toBe(true); 36 | expect(contextManager.stats.hits).toBe(1); 37 | expect(contextManager.stats.misses).toBe(1); 38 | }); 39 | 40 | it('should respect TTL settings', async () => { 41 | // Create context 42 | await contextManager.getContext('test-id', { test: true }); 43 | 44 | // Wait for TTL to expire 45 | await new Promise((resolve) => setTimeout(resolve, 1100)); 46 | 47 | // Should create new context 48 | await contextManager.getContext('test-id', { test: true }); 49 | expect(contextManager.stats.misses).toBe(2); 50 | expect(contextManager.stats.hits).toBe(0); 51 | }); 52 | }); 53 | 54 | describe('updateContext', () => { 55 | it('should update existing context metadata', async () => { 56 | await contextManager.getContext('test-id', { initial: true }); 57 | const updated = await contextManager.updateContext('test-id', { 58 | updated: true 59 | }); 60 | 61 | expect(updated.metadata.initial).toBe(true); 62 | expect(updated.metadata.updated).toBe(true); 63 | }); 64 | }); 65 | 66 | describe('invalidateContext', () => { 67 | it('should remove context from cache', async () => { 68 | await contextManager.getContext('test-id', { test: true }); 69 | contextManager.invalidateContext('test-id', { test: true }); 70 | 71 | // Should be a cache miss 72 | await contextManager.getContext('test-id', { test: true }); 73 | expect(contextManager.stats.invalidations).toBe(1); 74 | expect(contextManager.stats.misses).toBe(2); 75 | }); 76 | }); 77 | 78 | describe('getStats', () => { 79 | it('should return current cache statistics', async () => { 80 | await contextManager.getContext('test-id', { test: true }); 81 | const stats = contextManager.getStats(); 82 | 83 | expect(stats.hits).toBe(0); 84 | expect(stats.misses).toBe(1); 85 | expect(stats.invalidations).toBe(0); 86 | expect(stats.size).toBe(1); 87 | expect(stats.maxSize).toBe(10); 88 | expect(stats.ttl).toBe(1000); 89 | }); 90 | }); 91 | }); 92 | ``` -------------------------------------------------------------------------------- /tests/unit/profiles/rule-transformer-gemini.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import { getRulesProfile } from '../../../src/utils/rule-transformer.js'; 3 | import { geminiProfile } from '../../../src/profiles/gemini.js'; 4 | 5 | describe('Rule Transformer - Gemini Profile', () => { 6 | test('should have correct profile configuration', () => { 7 | const geminiProfile = getRulesProfile('gemini'); 8 | 9 | expect(geminiProfile).toBeDefined(); 10 | expect(geminiProfile.profileName).toBe('gemini'); 11 | expect(geminiProfile.displayName).toBe('Gemini'); 12 | expect(geminiProfile.profileDir).toBe('.gemini'); 13 | expect(geminiProfile.rulesDir).toBe('.'); 14 | expect(geminiProfile.mcpConfig).toBe(true); 15 | expect(geminiProfile.mcpConfigName).toBe('settings.json'); 16 | expect(geminiProfile.mcpConfigPath).toBe('.gemini/settings.json'); 17 | expect(geminiProfile.includeDefaultRules).toBe(false); 18 | expect(geminiProfile.fileMap).toEqual({ 19 | 'AGENTS.md': 'GEMINI.md' 20 | }); 21 | }); 22 | 23 | test('should have minimal profile implementation', () => { 24 | // Verify that gemini.js is minimal (no lifecycle functions) 25 | expect(geminiProfile.onAddRulesProfile).toBeUndefined(); 26 | expect(geminiProfile.onRemoveRulesProfile).toBeUndefined(); 27 | expect(geminiProfile.onPostConvertRulesProfile).toBeUndefined(); 28 | }); 29 | 30 | test('should use settings.json instead of mcp.json', () => { 31 | const geminiProfile = getRulesProfile('gemini'); 32 | expect(geminiProfile.mcpConfigName).toBe('settings.json'); 33 | expect(geminiProfile.mcpConfigPath).toBe('.gemini/settings.json'); 34 | }); 35 | 36 | test('should not include default rules', () => { 37 | const geminiProfile = getRulesProfile('gemini'); 38 | expect(geminiProfile.includeDefaultRules).toBe(false); 39 | }); 40 | 41 | test('should have correct file mapping', () => { 42 | const geminiProfile = getRulesProfile('gemini'); 43 | expect(geminiProfile.fileMap).toEqual({ 44 | 'AGENTS.md': 'GEMINI.md' 45 | }); 46 | }); 47 | 48 | test('should place GEMINI.md in root directory', () => { 49 | const geminiProfile = getRulesProfile('gemini'); 50 | // rulesDir determines where fileMap files go 51 | expect(geminiProfile.rulesDir).toBe('.'); 52 | // This means AGENTS.md -> GEMINI.md will be placed in the root 53 | }); 54 | 55 | test('should place settings.json in .gemini directory', () => { 56 | const geminiProfile = getRulesProfile('gemini'); 57 | // profileDir + mcpConfigName determines MCP config location 58 | expect(geminiProfile.profileDir).toBe('.gemini'); 59 | expect(geminiProfile.mcpConfigName).toBe('settings.json'); 60 | expect(geminiProfile.mcpConfigPath).toBe('.gemini/settings.json'); 61 | }); 62 | 63 | test('should have proper conversion config', () => { 64 | const geminiProfile = getRulesProfile('gemini'); 65 | // Gemini should have the standard conversion config 66 | expect(geminiProfile.conversionConfig).toBeDefined(); 67 | expect(geminiProfile.globalReplacements).toBeDefined(); 68 | expect(Array.isArray(geminiProfile.globalReplacements)).toBe(true); 69 | }); 70 | }); 71 | ``` -------------------------------------------------------------------------------- /apps/extension/src/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * Logger interface for dependency injection 5 | */ 6 | export interface ILogger { 7 | log(message: string, ...args: any[]): void; 8 | error(message: string, ...args: any[]): void; 9 | warn(message: string, ...args: any[]): void; 10 | debug(message: string, ...args: any[]): void; 11 | show(): void; 12 | dispose(): void; 13 | } 14 | 15 | /** 16 | * Logger that outputs to VS Code's output channel instead of console 17 | * This prevents interference with MCP stdio communication 18 | */ 19 | export class ExtensionLogger implements ILogger { 20 | private static instance: ExtensionLogger; 21 | private outputChannel: vscode.OutputChannel; 22 | private debugMode: boolean; 23 | 24 | private constructor() { 25 | this.outputChannel = vscode.window.createOutputChannel('TaskMaster'); 26 | const config = vscode.workspace.getConfiguration('taskmaster'); 27 | this.debugMode = config.get<boolean>('debug.enableLogging', true); 28 | } 29 | 30 | static getInstance(): ExtensionLogger { 31 | if (!ExtensionLogger.instance) { 32 | ExtensionLogger.instance = new ExtensionLogger(); 33 | } 34 | return ExtensionLogger.instance; 35 | } 36 | 37 | log(message: string, ...args: any[]): void { 38 | if (!this.debugMode) { 39 | return; 40 | } 41 | const timestamp = new Date().toISOString(); 42 | const formattedMessage = this.formatMessage(message, args); 43 | this.outputChannel.appendLine(`[${timestamp}] ${formattedMessage}`); 44 | } 45 | 46 | error(message: string, ...args: any[]): void { 47 | const timestamp = new Date().toISOString(); 48 | const formattedMessage = this.formatMessage(message, args); 49 | this.outputChannel.appendLine(`[${timestamp}] ERROR: ${formattedMessage}`); 50 | } 51 | 52 | warn(message: string, ...args: any[]): void { 53 | if (!this.debugMode) { 54 | return; 55 | } 56 | const timestamp = new Date().toISOString(); 57 | const formattedMessage = this.formatMessage(message, args); 58 | this.outputChannel.appendLine(`[${timestamp}] WARN: ${formattedMessage}`); 59 | } 60 | 61 | debug(message: string, ...args: any[]): void { 62 | if (!this.debugMode) { 63 | return; 64 | } 65 | const timestamp = new Date().toISOString(); 66 | const formattedMessage = this.formatMessage(message, args); 67 | this.outputChannel.appendLine(`[${timestamp}] DEBUG: ${formattedMessage}`); 68 | } 69 | 70 | private formatMessage(message: string, args: any[]): string { 71 | if (args.length === 0) { 72 | return message; 73 | } 74 | 75 | // Convert objects to JSON for better readability 76 | const formattedArgs = args.map((arg) => { 77 | if (typeof arg === 'object' && arg !== null) { 78 | try { 79 | return JSON.stringify(arg, null, 2); 80 | } catch { 81 | return String(arg); 82 | } 83 | } 84 | return String(arg); 85 | }); 86 | 87 | return `${message} ${formattedArgs.join(' ')}`; 88 | } 89 | 90 | show(): void { 91 | this.outputChannel.show(); 92 | } 93 | 94 | dispose(): void { 95 | this.outputChannel.dispose(); 96 | } 97 | 98 | setDebugMode(enabled: boolean): void { 99 | this.debugMode = enabled; 100 | } 101 | } 102 | 103 | // Export a singleton instance for convenience 104 | export const logger = ExtensionLogger.getInstance(); 105 | ``` -------------------------------------------------------------------------------- /src/progress/progress-tracker-builder.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Configuration for progress tracker features 3 | */ 4 | class TrackerConfig { 5 | constructor() { 6 | this.features = new Set(); 7 | this.spinnerFrames = null; 8 | this.unitName = 'unit'; 9 | this.totalUnits = 100; 10 | } 11 | 12 | addFeature(feature) { 13 | this.features.add(feature); 14 | } 15 | 16 | hasFeature(feature) { 17 | return this.features.has(feature); 18 | } 19 | 20 | getOptions() { 21 | return { 22 | numUnits: this.totalUnits, 23 | unitName: this.unitName, 24 | spinnerFrames: this.spinnerFrames, 25 | features: Array.from(this.features) 26 | }; 27 | } 28 | } 29 | 30 | /** 31 | * Builder for creating configured progress trackers 32 | */ 33 | export class ProgressTrackerBuilder { 34 | constructor() { 35 | this.config = new TrackerConfig(); 36 | } 37 | 38 | withPercent() { 39 | this.config.addFeature('percent'); 40 | return this; 41 | } 42 | 43 | withTokens() { 44 | this.config.addFeature('tokens'); 45 | return this; 46 | } 47 | 48 | withTasks() { 49 | this.config.addFeature('tasks'); 50 | return this; 51 | } 52 | 53 | withSpinner(messages) { 54 | if (!messages || !Array.isArray(messages)) { 55 | throw new Error('Spinner messages must be an array'); 56 | } 57 | this.config.spinnerFrames = messages; 58 | return this; 59 | } 60 | 61 | withUnits(total, unitName = 'unit') { 62 | this.config.totalUnits = total; 63 | this.config.unitName = unitName; 64 | return this; 65 | } 66 | 67 | build() { 68 | return new ProgressTracker(this.config); 69 | } 70 | } 71 | 72 | /** 73 | * Base progress tracker with configurable features 74 | */ 75 | class ProgressTracker { 76 | constructor(config) { 77 | this.config = config; 78 | this.isActive = false; 79 | this.current = 0; 80 | this.spinnerIndex = 0; 81 | this.startTime = null; 82 | } 83 | 84 | start() { 85 | this.isActive = true; 86 | this.startTime = Date.now(); 87 | this.current = 0; 88 | 89 | if (this.config.spinnerFrames) { 90 | this._startSpinner(); 91 | } 92 | } 93 | 94 | update(data = {}) { 95 | if (!this.isActive) return; 96 | 97 | if (data.current !== undefined) { 98 | this.current = data.current; 99 | } 100 | 101 | const progress = this._buildProgressData(data); 102 | return progress; 103 | } 104 | 105 | finish() { 106 | this.isActive = false; 107 | 108 | if (this.spinnerInterval) { 109 | clearInterval(this.spinnerInterval); 110 | this.spinnerInterval = null; 111 | } 112 | 113 | return this._buildSummary(); 114 | } 115 | 116 | _startSpinner() { 117 | this.spinnerInterval = setInterval(() => { 118 | this.spinnerIndex = 119 | (this.spinnerIndex + 1) % this.config.spinnerFrames.length; 120 | }, 100); 121 | } 122 | 123 | _buildProgressData(data) { 124 | const progress = { ...data }; 125 | 126 | if (this.config.hasFeature('percent')) { 127 | progress.percentage = Math.round( 128 | (this.current / this.config.totalUnits) * 100 129 | ); 130 | } 131 | 132 | if (this.config.hasFeature('tasks')) { 133 | progress.tasks = `${this.current}/${this.config.totalUnits}`; 134 | } 135 | 136 | if (this.config.spinnerFrames) { 137 | progress.spinner = this.config.spinnerFrames[this.spinnerIndex]; 138 | } 139 | 140 | return progress; 141 | } 142 | 143 | _buildSummary() { 144 | const elapsed = Date.now() - this.startTime; 145 | return { 146 | total: this.config.totalUnits, 147 | completed: this.current, 148 | elapsedMs: elapsed, 149 | features: Array.from(this.config.features) 150 | }; 151 | } 152 | } 153 | ``` -------------------------------------------------------------------------------- /src/profiles/kiro.js: -------------------------------------------------------------------------------- ```javascript 1 | // Kiro profile for rule-transformer 2 | import { createProfile } from './base-profile.js'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { log } from '../../scripts/modules/utils.js'; 6 | 7 | // Create and export kiro profile using the base factory 8 | export const kiroProfile = createProfile({ 9 | name: 'kiro', 10 | displayName: 'Kiro', 11 | url: 'kiro.dev', 12 | docsUrl: 'kiro.dev/docs', 13 | profileDir: '.kiro', 14 | rulesDir: '.kiro/steering', // Kiro rules location (full path) 15 | mcpConfig: true, 16 | mcpConfigName: 'settings/mcp.json', // Create directly in settings subdirectory 17 | includeDefaultRules: true, // Include default rules to get all the standard files 18 | targetExtension: '.md', 19 | fileMap: { 20 | // Override specific mappings - the base profile will create: 21 | // 'rules/cursor_rules.mdc': 'kiro_rules.md' 22 | // 'rules/dev_workflow.mdc': 'dev_workflow.md' 23 | // 'rules/self_improve.mdc': 'self_improve.md' 24 | // 'rules/taskmaster.mdc': 'taskmaster.md' 25 | // We can add additional custom mappings here if needed 26 | 'rules/taskmaster_hooks_workflow.mdc': 'taskmaster_hooks_workflow.md' 27 | }, 28 | customReplacements: [ 29 | // Core Kiro directory structure changes 30 | { from: /\.cursor\/rules/g, to: '.kiro/steering' }, 31 | { from: /\.cursor\/mcp\.json/g, to: '.kiro/settings/mcp.json' }, 32 | 33 | // Fix any remaining kiro/rules references that might be created during transformation 34 | { from: /\.kiro\/rules/g, to: '.kiro/steering' }, 35 | 36 | // Essential markdown link transformations for Kiro structure 37 | { 38 | from: /\[(.+?)\]\(mdc:\.cursor\/rules\/(.+?)\.mdc\)/g, 39 | to: '[$1](.kiro/steering/$2.md)' 40 | }, 41 | 42 | // Kiro specific terminology 43 | { from: /rules directory/g, to: 'steering directory' }, 44 | { from: /cursor rules/gi, to: 'Kiro steering files' }, 45 | 46 | // Transform frontmatter to Kiro format 47 | // This regex matches the entire frontmatter block and replaces it 48 | { 49 | from: /^---\n(?:description:\s*[^\n]*\n)?(?:globs:\s*[^\n]*\n)?(?:alwaysApply:\s*true\n)?---/m, 50 | to: '---\ninclusion: always\n---' 51 | } 52 | ], 53 | 54 | // Add lifecycle hook to copy Kiro hooks 55 | onPostConvert: (projectRoot, assetsDir) => { 56 | const hooksSourceDir = path.join(assetsDir, 'kiro-hooks'); 57 | const hooksTargetDir = path.join(projectRoot, '.kiro', 'hooks'); 58 | 59 | // Create hooks directory if it doesn't exist 60 | if (!fs.existsSync(hooksTargetDir)) { 61 | fs.mkdirSync(hooksTargetDir, { recursive: true }); 62 | } 63 | 64 | // Copy all .kiro.hook files 65 | if (fs.existsSync(hooksSourceDir)) { 66 | const hookFiles = fs 67 | .readdirSync(hooksSourceDir) 68 | .filter((f) => f.endsWith('.kiro.hook')); 69 | 70 | hookFiles.forEach((file) => { 71 | const sourcePath = path.join(hooksSourceDir, file); 72 | const targetPath = path.join(hooksTargetDir, file); 73 | 74 | fs.copyFileSync(sourcePath, targetPath); 75 | }); 76 | 77 | if (hookFiles.length > 0) { 78 | log( 79 | 'info', 80 | `[Kiro] Installed ${hookFiles.length} Taskmaster hooks in .kiro/hooks/` 81 | ); 82 | } 83 | } 84 | } 85 | }); 86 | ``` -------------------------------------------------------------------------------- /apps/extension/src/services/notification-preferences.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * Notification Preferences Service 3 | * Manages user preferences for notifications 4 | */ 5 | 6 | import * as vscode from 'vscode'; 7 | import { ErrorCategory, ErrorSeverity } from './error-handler'; 8 | 9 | export enum NotificationLevel { 10 | ALL = 'all', 11 | ERRORS_ONLY = 'errors_only', 12 | CRITICAL_ONLY = 'critical_only', 13 | NONE = 'none' 14 | } 15 | 16 | interface NotificationRule { 17 | category: ErrorCategory; 18 | minSeverity: ErrorSeverity; 19 | enabled: boolean; 20 | } 21 | 22 | export class NotificationPreferences { 23 | private defaultRules: NotificationRule[] = [ 24 | { 25 | category: ErrorCategory.MCP_CONNECTION, 26 | minSeverity: ErrorSeverity.HIGH, 27 | enabled: true 28 | }, 29 | { 30 | category: ErrorCategory.CONFIGURATION, 31 | minSeverity: ErrorSeverity.MEDIUM, 32 | enabled: true 33 | }, 34 | { 35 | category: ErrorCategory.TASK_LOADING, 36 | minSeverity: ErrorSeverity.HIGH, 37 | enabled: true 38 | }, 39 | { 40 | category: ErrorCategory.NETWORK, 41 | minSeverity: ErrorSeverity.HIGH, 42 | enabled: true 43 | }, 44 | { 45 | category: ErrorCategory.INTERNAL, 46 | minSeverity: ErrorSeverity.CRITICAL, 47 | enabled: true 48 | } 49 | ]; 50 | 51 | /** 52 | * Check if a notification should be shown 53 | */ 54 | shouldShowNotification( 55 | category: ErrorCategory, 56 | severity: ErrorSeverity 57 | ): boolean { 58 | // Get user's notification level preference 59 | const level = this.getNotificationLevel(); 60 | 61 | if (level === NotificationLevel.NONE) { 62 | return false; 63 | } 64 | 65 | if ( 66 | level === NotificationLevel.CRITICAL_ONLY && 67 | severity !== ErrorSeverity.CRITICAL 68 | ) { 69 | return false; 70 | } 71 | 72 | if ( 73 | level === NotificationLevel.ERRORS_ONLY && 74 | severity !== ErrorSeverity.CRITICAL && 75 | severity !== ErrorSeverity.HIGH 76 | ) { 77 | return false; 78 | } 79 | 80 | // Check category-specific rules 81 | const rule = this.defaultRules.find((r) => r.category === category); 82 | if (!rule || !rule.enabled) { 83 | return false; 84 | } 85 | 86 | // Check if severity meets minimum threshold 87 | return this.compareSeverity(severity, rule.minSeverity) >= 0; 88 | } 89 | 90 | /** 91 | * Get user's notification level preference 92 | */ 93 | private getNotificationLevel(): NotificationLevel { 94 | const config = vscode.workspace.getConfiguration('taskmaster'); 95 | return config.get<NotificationLevel>( 96 | 'notifications.level', 97 | NotificationLevel.ERRORS_ONLY 98 | ); 99 | } 100 | 101 | /** 102 | * Compare severity levels 103 | */ 104 | private compareSeverity(a: ErrorSeverity, b: ErrorSeverity): number { 105 | const severityOrder = { 106 | [ErrorSeverity.LOW]: 0, 107 | [ErrorSeverity.MEDIUM]: 1, 108 | [ErrorSeverity.HIGH]: 2, 109 | [ErrorSeverity.CRITICAL]: 3 110 | }; 111 | return severityOrder[a] - severityOrder[b]; 112 | } 113 | 114 | /** 115 | * Get toast notification duration based on severity 116 | */ 117 | getToastDuration(severity: ErrorSeverity): number { 118 | switch (severity) { 119 | case ErrorSeverity.CRITICAL: 120 | return 10000; // 10 seconds 121 | case ErrorSeverity.HIGH: 122 | return 7000; // 7 seconds 123 | case ErrorSeverity.MEDIUM: 124 | return 5000; // 5 seconds 125 | case ErrorSeverity.LOW: 126 | return 3000; // 3 seconds 127 | } 128 | } 129 | } 130 | ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/expand-task.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * tools/expand-task.js 3 | * Tool to expand a task into subtasks 4 | */ 5 | 6 | import { z } from 'zod'; 7 | import { 8 | handleApiResult, 9 | createErrorResponse, 10 | withNormalizedProjectRoot 11 | } from './utils.js'; 12 | import { expandTaskDirect } from '../core/task-master-core.js'; 13 | import { 14 | findTasksPath, 15 | findComplexityReportPath 16 | } from '../core/utils/path-utils.js'; 17 | import { resolveTag } from '../../../scripts/modules/utils.js'; 18 | 19 | /** 20 | * Register the expand-task tool with the MCP server 21 | * @param {Object} server - FastMCP server instance 22 | */ 23 | export function registerExpandTaskTool(server) { 24 | server.addTool({ 25 | name: 'expand_task', 26 | description: 'Expand a task into subtasks for detailed implementation', 27 | parameters: z.object({ 28 | id: z.string().describe('ID of task to expand'), 29 | num: z.string().optional().describe('Number of subtasks to generate'), 30 | research: z 31 | .boolean() 32 | .optional() 33 | .default(false) 34 | .describe('Use research role for generation'), 35 | prompt: z 36 | .string() 37 | .optional() 38 | .describe('Additional context for subtask generation'), 39 | file: z 40 | .string() 41 | .optional() 42 | .describe( 43 | 'Path to the tasks file relative to project root (e.g., tasks/tasks.json)' 44 | ), 45 | projectRoot: z 46 | .string() 47 | .describe('The directory of the project. Must be an absolute path.'), 48 | force: z 49 | .boolean() 50 | .optional() 51 | .default(false) 52 | .describe('Force expansion even if subtasks exist'), 53 | tag: z.string().optional().describe('Tag context to operate on') 54 | }), 55 | execute: withNormalizedProjectRoot(async (args, { log, session }) => { 56 | try { 57 | log.info(`Starting expand-task with args: ${JSON.stringify(args)}`); 58 | const resolvedTag = resolveTag({ 59 | projectRoot: args.projectRoot, 60 | tag: args.tag 61 | }); 62 | // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) 63 | let tasksJsonPath; 64 | try { 65 | tasksJsonPath = findTasksPath( 66 | { projectRoot: args.projectRoot, file: args.file }, 67 | log 68 | ); 69 | } catch (error) { 70 | log.error(`Error finding tasks.json: ${error.message}`); 71 | return createErrorResponse( 72 | `Failed to find tasks.json: ${error.message}` 73 | ); 74 | } 75 | 76 | const complexityReportPath = findComplexityReportPath( 77 | { ...args, tag: resolvedTag }, 78 | log 79 | ); 80 | 81 | const result = await expandTaskDirect( 82 | { 83 | tasksJsonPath: tasksJsonPath, 84 | id: args.id, 85 | num: args.num, 86 | research: args.research, 87 | prompt: args.prompt, 88 | force: args.force, 89 | complexityReportPath, 90 | projectRoot: args.projectRoot, 91 | tag: resolvedTag 92 | }, 93 | log, 94 | { session } 95 | ); 96 | 97 | return handleApiResult( 98 | result, 99 | log, 100 | 'Error expanding task', 101 | undefined, 102 | args.projectRoot 103 | ); 104 | } catch (error) { 105 | log.error(`Error in expand-task tool: ${error.message}`); 106 | return createErrorResponse(error.message); 107 | } 108 | }) 109 | }); 110 | } 111 | ``` -------------------------------------------------------------------------------- /apps/extension/src/utils/task-master-api/types/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * TaskMaster API Types 3 | * All type definitions for the TaskMaster API 4 | */ 5 | 6 | // MCP Response Types 7 | export interface MCPTaskResponse { 8 | data?: { 9 | tasks?: Array<{ 10 | id: number | string; 11 | title: string; 12 | description: string; 13 | status: string; 14 | priority: string; 15 | details?: string; 16 | testStrategy?: string; 17 | dependencies?: Array<number | string>; 18 | complexityScore?: number; 19 | subtasks?: Array<{ 20 | id: number; 21 | title: string; 22 | description?: string; 23 | status: string; 24 | details?: string; 25 | dependencies?: Array<number | string>; 26 | }>; 27 | }>; 28 | tag?: { 29 | currentTag: string; 30 | availableTags: string[]; 31 | }; 32 | }; 33 | version?: { 34 | version: string; 35 | name: string; 36 | }; 37 | error?: string; 38 | } 39 | 40 | // Internal Task Interface 41 | export interface TaskMasterTask { 42 | id: string; 43 | title: string; 44 | description: string; 45 | status: 46 | | 'pending' 47 | | 'in-progress' 48 | | 'review' 49 | | 'done' 50 | | 'deferred' 51 | | 'cancelled'; 52 | priority: 'high' | 'medium' | 'low'; 53 | details?: string; 54 | testStrategy?: string; 55 | dependencies?: string[]; 56 | complexityScore?: number; 57 | subtasks?: Array<{ 58 | id: number; 59 | title: string; 60 | description?: string; 61 | status: string; 62 | details?: string; 63 | testStrategy?: string; 64 | dependencies?: Array<number | string>; 65 | }>; 66 | } 67 | 68 | // API Response Wrapper 69 | export interface TaskMasterApiResponse<T = any> { 70 | success: boolean; 71 | data?: T; 72 | error?: string; 73 | requestDuration?: number; 74 | } 75 | 76 | // API Configuration 77 | export interface TaskMasterApiConfig { 78 | timeout: number; 79 | retryAttempts: number; 80 | cacheDuration: number; 81 | projectRoot?: string; 82 | cache?: CacheConfig; 83 | } 84 | 85 | export interface CacheConfig { 86 | maxSize: number; 87 | enableBackgroundRefresh: boolean; 88 | refreshInterval: number; 89 | enableAnalytics: boolean; 90 | enablePrefetch: boolean; 91 | compressionEnabled: boolean; 92 | persistToDisk: boolean; 93 | } 94 | 95 | // Cache Types 96 | export interface CacheEntry { 97 | data: any; 98 | timestamp: number; 99 | accessCount: number; 100 | lastAccessed: number; 101 | size: number; 102 | ttl?: number; 103 | tags: string[]; 104 | } 105 | 106 | export interface CacheAnalytics { 107 | hits: number; 108 | misses: number; 109 | evictions: number; 110 | refreshes: number; 111 | totalSize: number; 112 | averageAccessTime: number; 113 | hitRate: number; 114 | } 115 | 116 | // Method Options 117 | export interface GetTasksOptions { 118 | status?: string; 119 | withSubtasks?: boolean; 120 | tag?: string; 121 | projectRoot?: string; 122 | } 123 | 124 | export interface UpdateTaskStatusOptions { 125 | projectRoot?: string; 126 | } 127 | 128 | export interface UpdateTaskOptions { 129 | projectRoot?: string; 130 | append?: boolean; 131 | research?: boolean; 132 | } 133 | 134 | export interface UpdateSubtaskOptions { 135 | projectRoot?: string; 136 | research?: boolean; 137 | } 138 | 139 | export interface AddSubtaskOptions { 140 | projectRoot?: string; 141 | } 142 | 143 | export interface TaskUpdate { 144 | title?: string; 145 | description?: string; 146 | details?: string; 147 | priority?: 'high' | 'medium' | 'low'; 148 | testStrategy?: string; 149 | dependencies?: string[]; 150 | } 151 | 152 | export interface SubtaskData { 153 | title: string; 154 | description?: string; 155 | dependencies?: string[]; 156 | status?: string; 157 | } 158 | ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/remove-dependency.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Direct function wrapper for removeDependency 3 | */ 4 | 5 | import { removeDependency } from '../../../../scripts/modules/dependency-manager.js'; 6 | import { 7 | enableSilentMode, 8 | disableSilentMode 9 | } from '../../../../scripts/modules/utils.js'; 10 | 11 | /** 12 | * Remove a dependency from a task 13 | * @param {Object} args - Function arguments 14 | * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. 15 | * @param {string|number} args.id - Task ID to remove dependency from 16 | * @param {string|number} args.dependsOn - Task ID to remove as a dependency 17 | * @param {string} args.projectRoot - Project root path (for MCP/env fallback) 18 | * @param {string} args.tag - Tag for the task (optional) 19 | * @param {Object} log - Logger object 20 | * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>} 21 | */ 22 | export async function removeDependencyDirect(args, log) { 23 | // Destructure expected args 24 | const { tasksJsonPath, id, dependsOn, projectRoot, tag } = args; 25 | try { 26 | log.info(`Removing dependency with args: ${JSON.stringify(args)}`); 27 | 28 | // Check if tasksJsonPath was provided 29 | if (!tasksJsonPath) { 30 | log.error('removeDependencyDirect called without tasksJsonPath'); 31 | return { 32 | success: false, 33 | error: { 34 | code: 'MISSING_ARGUMENT', 35 | message: 'tasksJsonPath is required' 36 | } 37 | }; 38 | } 39 | 40 | // Validate required parameters 41 | if (!id) { 42 | return { 43 | success: false, 44 | error: { 45 | code: 'INPUT_VALIDATION_ERROR', 46 | message: 'Task ID (id) is required' 47 | } 48 | }; 49 | } 50 | 51 | if (!dependsOn) { 52 | return { 53 | success: false, 54 | error: { 55 | code: 'INPUT_VALIDATION_ERROR', 56 | message: 'Dependency ID (dependsOn) is required' 57 | } 58 | }; 59 | } 60 | 61 | // Use provided path 62 | const tasksPath = tasksJsonPath; 63 | 64 | // Format IDs for the core function 65 | const taskId = 66 | id && id.includes && id.includes('.') ? id : parseInt(id, 10); 67 | const dependencyId = 68 | dependsOn && dependsOn.includes && dependsOn.includes('.') 69 | ? dependsOn 70 | : parseInt(dependsOn, 10); 71 | 72 | log.info( 73 | `Removing dependency: task ${taskId} no longer depends on ${dependencyId}` 74 | ); 75 | 76 | // Enable silent mode to prevent console logs from interfering with JSON response 77 | enableSilentMode(); 78 | 79 | // Call the core function using the provided tasksPath 80 | await removeDependency(tasksPath, taskId, dependencyId, { 81 | projectRoot, 82 | tag 83 | }); 84 | 85 | // Restore normal logging 86 | disableSilentMode(); 87 | 88 | return { 89 | success: true, 90 | data: { 91 | message: `Successfully removed dependency: Task ${taskId} no longer depends on ${dependencyId}`, 92 | taskId: taskId, 93 | dependencyId: dependencyId 94 | } 95 | }; 96 | } catch (error) { 97 | // Make sure to restore normal logging even if there's an error 98 | disableSilentMode(); 99 | 100 | log.error(`Error in removeDependencyDirect: ${error.message}`); 101 | return { 102 | success: false, 103 | error: { 104 | code: 'CORE_FUNCTION_ERROR', 105 | message: error.message 106 | } 107 | }; 108 | } 109 | } 110 | ``` -------------------------------------------------------------------------------- /apps/docs/capabilities/mcp.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: MCP Tools 3 | sidebarTitle: "MCP Tools" 4 | --- 5 | 6 | # MCP Tools 7 | 8 | This document provides an overview of the MCP (Machine-to-Machine Communication Protocol) interface for the Task Master application. The MCP interface is defined in the `mcp-server/` directory and exposes the application's core functionalities as a set of tools that can be called remotely. 9 | 10 | ## Core Concepts 11 | 12 | The MCP interface is built on top of the `fastmcp` library and registers a set of tools that correspond to the core functionalities of the Task Master application. These tools are defined in the `mcp-server/src/tools/` directory and are registered with the MCP server in `mcp-server/src/tools/index.js`. 13 | 14 | Each tool is defined with a name, a description, and a set of parameters that are validated using the `zod` library. The `execute` function of each tool calls the corresponding core logic function from `scripts/modules/task-manager.js`. 15 | 16 | ## Tool Categories 17 | 18 | The MCP tools can be categorized in the same way as the core functionalities: 19 | 20 | ### 1. Task and Subtask Management 21 | 22 | - **`add_task`**: Creates a new task. 23 | - **`add_subtask`**: Adds a subtask to a parent task. 24 | - **`remove_task`**: Removes one or more tasks or subtasks. 25 | - **`remove_subtask`**: Removes a subtask from its parent. 26 | - **`update_task`**: Updates a single task. 27 | - **`update_subtask`**: Appends information to a subtask. 28 | - **`update`**: Updates multiple tasks. 29 | - **`move_task`**: Moves a task or subtask. 30 | - **`clear_subtasks`**: Clears all subtasks from one or more tasks. 31 | 32 | ### 2. Task Information and Status 33 | 34 | - **`get_tasks`**: Lists all tasks. 35 | - **`get_task`**: Shows the details of a specific task. 36 | - **`next_task`**: Shows the next task to work on. 37 | - **`set_task_status`**: Sets the status of a task or subtask. 38 | 39 | ### 3. Task Analysis and Expansion 40 | 41 | - **`parse_prd`**: Parses a PRD to generate tasks. 42 | - **`expand_task`**: Expands a task into subtasks. 43 | - **`expand_all`**: Expands all eligible tasks. 44 | - **`analyze_project_complexity`**: Analyzes task complexity. 45 | - **`complexity_report`**: Displays the complexity analysis report. 46 | 47 | ### 4. Dependency Management 48 | 49 | - **`add_dependency`**: Adds a dependency to a task. 50 | - **`remove_dependency`**: Removes a dependency from a task. 51 | - **`validate_dependencies`**: Validates the dependencies of all tasks. 52 | - **`fix_dependencies`**: Fixes any invalid dependencies. 53 | 54 | ### 5. Project and Configuration 55 | 56 | - **`initialize_project`**: Initializes a new project. 57 | - **`generate`**: Generates individual task files. 58 | - **`models`**: Manages AI model configurations. 59 | - **`research`**: Performs AI-powered research. 60 | 61 | ### 6. Tag Management 62 | 63 | - **`add_tag`**: Creates a new tag. 64 | - **`delete_tag`**: Deletes a tag. 65 | - **`list_tags`**: Lists all tags. 66 | - **`use_tag`**: Switches to a different tag. 67 | - **`rename_tag`**: Renames a tag. 68 | - **`copy_tag`**: Copies a tag. ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/delete-tag.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * delete-tag.js 3 | * Direct function implementation for deleting a tag 4 | */ 5 | 6 | import { deleteTag } from '../../../../scripts/modules/task-manager/tag-management.js'; 7 | import { 8 | enableSilentMode, 9 | disableSilentMode 10 | } from '../../../../scripts/modules/utils.js'; 11 | import { createLogWrapper } from '../../tools/utils.js'; 12 | 13 | /** 14 | * Direct function wrapper for deleting a tag with error handling. 15 | * 16 | * @param {Object} args - Command arguments 17 | * @param {string} args.name - Name of the tag to delete 18 | * @param {boolean} [args.yes=false] - Skip confirmation prompts 19 | * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) 20 | * @param {string} [args.projectRoot] - Project root path 21 | * @param {Object} log - Logger object 22 | * @param {Object} context - Additional context (session) 23 | * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } 24 | */ 25 | export async function deleteTagDirect(args, log, context = {}) { 26 | // Destructure expected args 27 | const { tasksJsonPath, name, yes = false, projectRoot } = args; 28 | const { session } = context; 29 | 30 | // Enable silent mode to prevent console logs from interfering with JSON response 31 | enableSilentMode(); 32 | 33 | // Create logger wrapper using the utility 34 | const mcpLog = createLogWrapper(log); 35 | 36 | try { 37 | // Check if tasksJsonPath was provided 38 | if (!tasksJsonPath) { 39 | log.error('deleteTagDirect called without tasksJsonPath'); 40 | disableSilentMode(); 41 | return { 42 | success: false, 43 | error: { 44 | code: 'MISSING_ARGUMENT', 45 | message: 'tasksJsonPath is required' 46 | } 47 | }; 48 | } 49 | 50 | // Check required parameters 51 | if (!name || typeof name !== 'string') { 52 | log.error('Missing required parameter: name'); 53 | disableSilentMode(); 54 | return { 55 | success: false, 56 | error: { 57 | code: 'MISSING_PARAMETER', 58 | message: 'Tag name is required and must be a string' 59 | } 60 | }; 61 | } 62 | 63 | log.info(`Deleting tag: ${name}`); 64 | 65 | // Prepare options 66 | const options = { 67 | yes // For MCP, we always skip confirmation prompts 68 | }; 69 | 70 | // Call the deleteTag function 71 | const result = await deleteTag( 72 | tasksJsonPath, 73 | name, 74 | options, 75 | { 76 | session, 77 | mcpLog, 78 | projectRoot 79 | }, 80 | 'json' // outputFormat - use 'json' to suppress CLI UI 81 | ); 82 | 83 | // Restore normal logging 84 | disableSilentMode(); 85 | 86 | return { 87 | success: true, 88 | data: { 89 | tagName: result.tagName, 90 | deleted: result.deleted, 91 | tasksDeleted: result.tasksDeleted, 92 | wasCurrentTag: result.wasCurrentTag, 93 | switchedToMaster: result.switchedToMaster, 94 | message: `Successfully deleted tag "${result.tagName}"` 95 | } 96 | }; 97 | } catch (error) { 98 | // Make sure to restore normal logging even if there's an error 99 | disableSilentMode(); 100 | 101 | log.error(`Error in deleteTagDirect: ${error.message}`); 102 | return { 103 | success: false, 104 | error: { 105 | code: error.code || 'DELETE_TAG_ERROR', 106 | message: error.message 107 | } 108 | }; 109 | } 110 | } 111 | ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/update-task-by-id.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | 3 | jest.unstable_mockModule('fs', () => { 4 | const mockFs = { 5 | existsSync: jest.fn(() => true), 6 | writeFileSync: jest.fn(), 7 | readFileSync: jest.fn(), 8 | unlinkSync: jest.fn() 9 | }; 10 | return { default: mockFs, ...mockFs }; 11 | }); 12 | 13 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ 14 | readJSON: jest.fn(), 15 | writeJSON: jest.fn(), 16 | log: jest.fn(), 17 | isSilentMode: jest.fn(() => false), 18 | findProjectRoot: jest.fn(() => '/project'), 19 | flattenTasksWithSubtasks: jest.fn(() => []), 20 | truncate: jest.fn((t) => t), 21 | isEmpty: jest.fn(() => false), 22 | resolveEnvVariable: jest.fn(), 23 | findTaskById: jest.fn(), 24 | getCurrentTag: jest.fn(() => 'master') 25 | })); 26 | 27 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({ 28 | getStatusWithColor: jest.fn((s) => s), 29 | startLoadingIndicator: jest.fn(() => ({ stop: jest.fn() })), 30 | stopLoadingIndicator: jest.fn(), 31 | displayAiUsageSummary: jest.fn() 32 | })); 33 | 34 | jest.unstable_mockModule( 35 | '../../../../../scripts/modules/task-manager/generate-task-files.js', 36 | () => ({ 37 | default: jest.fn().mockResolvedValue() 38 | }) 39 | ); 40 | 41 | jest.unstable_mockModule( 42 | '../../../../../scripts/modules/ai-services-unified.js', 43 | () => ({ 44 | generateTextService: jest 45 | .fn() 46 | .mockResolvedValue({ mainResult: { content: '{}' }, telemetryData: {} }) 47 | }) 48 | ); 49 | 50 | jest.unstable_mockModule( 51 | '../../../../../scripts/modules/config-manager.js', 52 | () => ({ 53 | getDebugFlag: jest.fn(() => false), 54 | isApiKeySet: jest.fn(() => true), 55 | hasCodebaseAnalysis: jest.fn(() => false) 56 | }) 57 | ); 58 | 59 | const { readJSON, log } = await import( 60 | '../../../../../scripts/modules/utils.js' 61 | ); 62 | const { default: updateTaskById } = await import( 63 | '../../../../../scripts/modules/task-manager/update-task-by-id.js' 64 | ); 65 | 66 | describe('updateTaskById validation', () => { 67 | beforeEach(() => { 68 | jest.clearAllMocks(); 69 | jest.spyOn(process, 'exit').mockImplementation(() => { 70 | throw new Error('process.exit called'); 71 | }); 72 | }); 73 | 74 | test('throws error if prompt is empty', async () => { 75 | await expect( 76 | updateTaskById( 77 | 'tasks/tasks.json', 78 | 1, 79 | '', 80 | false, 81 | { tag: 'master' }, 82 | 'json' 83 | ) 84 | ).rejects.toThrow('Prompt cannot be empty'); 85 | }); 86 | 87 | test('throws error if task file missing', async () => { 88 | const fs = await import('fs'); 89 | fs.existsSync.mockReturnValue(false); 90 | await expect( 91 | updateTaskById( 92 | 'tasks/tasks.json', 93 | 1, 94 | 'prompt', 95 | false, 96 | { 97 | tag: 'master' 98 | }, 99 | 'json' 100 | ) 101 | ).rejects.toThrow('Tasks file not found'); 102 | }); 103 | 104 | test('throws error when task ID not found', async () => { 105 | const fs = await import('fs'); 106 | fs.existsSync.mockReturnValue(true); 107 | readJSON.mockReturnValue({ tag: 'master', tasks: [] }); 108 | await expect( 109 | updateTaskById( 110 | 'tasks/tasks.json', 111 | 42, 112 | 'prompt', 113 | false, 114 | { 115 | tag: 'master' 116 | }, 117 | 'json' 118 | ) 119 | ).rejects.toThrow('Task with ID 42 not found'); 120 | expect(log).toHaveBeenCalled(); 121 | }); 122 | }); 123 | ``` -------------------------------------------------------------------------------- /apps/docs/archive/cursor-setup.mdx: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | title: "Cursor AI Integration" 3 | description: "Learn how to set up and use Task Master with Cursor AI" 4 | --- 5 | 6 | ## Setting up Cursor AI Integration 7 | 8 | <Check> 9 | Task Master is designed to work seamlessly with [Cursor AI](https://www.cursor.so/), providing a structured workflow for AI-driven development. 10 | </Check> 11 | 12 | <AccordionGroup> 13 | <Accordion title="Using Cursor with MCP (Recommended)" icon="sparkles"> 14 | If you've already set up Task Master with MCP in Cursor, the integration is automatic. You can simply use natural language to interact with Task Master: 15 | 16 | ``` 17 | What tasks are available to work on next? 18 | Can you analyze the complexity of our tasks? 19 | I'd like to implement task 4. What does it involve? 20 | ``` 21 | </Accordion> 22 | <Accordion title="Manual Cursor Setup"> 23 | If you're not using MCP, you can still set up Cursor integration: 24 | 25 | <Steps> 26 | <Step title="After initializing your project, open it in Cursor"> 27 | The `.cursor/rules/dev_workflow.mdc` file is automatically loaded by Cursor, providing the AI with knowledge about the task management system 28 | </Step> 29 | <Step title="Place your PRD document in the scripts/ directory (e.g., scripts/prd.txt)"> 30 | 31 | </Step> 32 | <Step title="Open Cursor's AI chat and switch to Agent mode"> 33 | 34 | </Step> 35 | </Steps> 36 | </Accordion> 37 | <Accordion title="Alternative MCP Setup in Cursor"> 38 | <Steps> 39 | <Step title="Go to Cursor settings"> 40 | 41 | </Step> 42 | <Step title="Navigate to the MCP section"> 43 | 44 | </Step> 45 | <Step title="Click on 'Add New MCP Server'"> 46 | 47 | </Step> 48 | <Step title="Configure with the following details:"> 49 | - Name: "Task Master" 50 | - Type: "Command" 51 | - Command: "npx -y task-master-ai" 52 | </Step> 53 | <Step title="Save Settings"> 54 | 55 | </Step> 56 | </Steps> 57 | Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience. 58 | </Accordion> 59 | </AccordionGroup> 60 | 61 | ## Initial Task Generation 62 | 63 | In Cursor's AI chat, instruct the agent to generate tasks from your PRD: 64 | 65 | ``` 66 | Please use the task-master parse-prd command to generate tasks from my PRD. The PRD is located at scripts/prd.txt. 67 | ``` 68 | 69 | The agent will execute: 70 | 71 | ```bash 72 | task-master parse-prd scripts/prd.txt 73 | ``` 74 | 75 | This will: 76 | 77 | - Parse your PRD document 78 | - Generate a structured `tasks.json` file with tasks, dependencies, priorities, and test strategies 79 | - The agent will understand this process due to the Cursor rules 80 | 81 | ### Generate Individual Task Files 82 | 83 | Next, ask the agent to generate individual task files: 84 | 85 | ``` 86 | Please generate individual task files from tasks.json 87 | ``` 88 | 89 | The agent will execute: 90 | 91 | ```bash 92 | task-master generate 93 | ``` 94 | 95 | This creates individual task files in the `tasks/` directory (e.g., `task_001.txt`, `task_002.txt`), making it easier to reference specific tasks. 96 | ```