This is page 11 of 38. Use http://codebase.md/eyaltoledano/claude-task-master?lines=false&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /tests/unit/prompt-manager.test.js: -------------------------------------------------------------------------------- ```javascript import { jest, beforeAll, afterAll, beforeEach, afterEach, describe, it, expect } from '@jest/globals'; // Import the actual PromptManager to test with real prompt files import { PromptManager } from '../../scripts/modules/prompt-manager.js'; // Mock only the console logging const originalLog = console.log; const originalWarn = console.warn; const originalError = console.error; beforeAll(() => { console.log = jest.fn(); console.warn = jest.fn(); console.error = jest.fn(); }); afterAll(() => { console.log = originalLog; console.warn = originalWarn; console.error = originalError; }); describe('PromptManager', () => { let promptManager; beforeEach(() => { promptManager = new PromptManager(); }); describe('constructor', () => { it('should initialize with prompts map', () => { expect(promptManager.prompts).toBeInstanceOf(Map); expect(promptManager.prompts.size).toBeGreaterThan(0); }); it('should initialize cache', () => { expect(promptManager.cache).toBeInstanceOf(Map); expect(promptManager.cache.size).toBe(0); }); it('should load all expected prompts', () => { expect(promptManager.prompts.has('analyze-complexity')).toBe(true); expect(promptManager.prompts.has('expand-task')).toBe(true); expect(promptManager.prompts.has('add-task')).toBe(true); expect(promptManager.prompts.has('research')).toBe(true); expect(promptManager.prompts.has('parse-prd')).toBe(true); expect(promptManager.prompts.has('update-task')).toBe(true); expect(promptManager.prompts.has('update-tasks')).toBe(true); expect(promptManager.prompts.has('update-subtask')).toBe(true); }); }); describe('loadPrompt', () => { it('should load and render a prompt from actual files', () => { // Test with an actual prompt that exists const result = promptManager.loadPrompt('research', { query: 'test query', projectContext: 'test context' }); expect(result.systemPrompt).toBeDefined(); expect(result.userPrompt).toBeDefined(); expect(result.userPrompt).toContain('test query'); }); it('should handle missing variables with empty string', () => { // Add a test prompt to the manager for testing variable substitution promptManager.prompts.set('test-prompt', { id: 'test-prompt', version: '1.0.0', description: 'Test prompt', prompts: { default: { system: 'System', user: 'Hello {{name}}, your age is {{age}}' } } }); const result = promptManager.loadPrompt('test-prompt', { name: 'John' }); expect(result.userPrompt).toBe('Hello John, your age is '); }); it('should throw error for non-existent template', () => { expect(() => { promptManager.loadPrompt('non-existent-prompt'); }).toThrow("Prompt template 'non-existent-prompt' not found"); }); it('should use cache for repeated calls', () => { // First call with a real prompt const result1 = promptManager.loadPrompt('research', { query: 'test' }); // Mark the result to verify cache is used result1._cached = true; // Second call with same parameters should return cached result const result2 = promptManager.loadPrompt('research', { query: 'test' }); expect(result2._cached).toBe(true); expect(result1).toBe(result2); // Same object reference }); it('should handle array variables', () => { promptManager.prompts.set('array-prompt', { id: 'array-prompt', version: '1.0.0', description: 'Test array prompt', prompts: { default: { system: 'System', user: '{{#each items}}Item: {{.}}\n{{/each}}' } } }); const result = promptManager.loadPrompt('array-prompt', { items: ['one', 'two', 'three'] }); // The actual implementation doesn't handle {{this}} properly, check what it does produce expect(result.userPrompt).toContain('Item:'); }); it('should handle conditional blocks', () => { promptManager.prompts.set('conditional-prompt', { id: 'conditional-prompt', version: '1.0.0', description: 'Test conditional prompt', prompts: { default: { system: 'System', user: '{{#if hasData}}Data exists{{else}}No data{{/if}}' } } }); const withData = promptManager.loadPrompt('conditional-prompt', { hasData: true }); expect(withData.userPrompt).toBe('Data exists'); const withoutData = promptManager.loadPrompt('conditional-prompt', { hasData: false }); expect(withoutData.userPrompt).toBe('No data'); }); }); describe('renderTemplate', () => { it('should handle nested objects', () => { const template = 'User: {{user.name}}, Age: {{user.age}}'; const variables = { user: { name: 'John', age: 30 } }; const result = promptManager.renderTemplate(template, variables); expect(result).toBe('User: John, Age: 30'); }); it('should handle special characters in templates', () => { const template = 'Special: {{special}}'; const variables = { special: '<>&"\'' }; const result = promptManager.renderTemplate(template, variables); expect(result).toBe('Special: <>&"\''); }); }); describe('listPrompts', () => { it('should return all prompt IDs', () => { const prompts = promptManager.listPrompts(); expect(prompts).toBeInstanceOf(Array); expect(prompts.length).toBeGreaterThan(0); const ids = prompts.map((p) => p.id); expect(ids).toContain('analyze-complexity'); expect(ids).toContain('expand-task'); expect(ids).toContain('add-task'); expect(ids).toContain('research'); }); }); describe('validateTemplate', () => { it('should validate a correct template', () => { const result = promptManager.validateTemplate('research'); expect(result.valid).toBe(true); }); it('should reject invalid template', () => { const result = promptManager.validateTemplate('non-existent'); expect(result.valid).toBe(false); expect(result.error).toContain('not found'); }); }); }); ``` -------------------------------------------------------------------------------- /.taskmaster/docs/research/2025-06-14_should-i-be-using-any-specific-libraries-for-this.md: -------------------------------------------------------------------------------- ```markdown --- title: Research Session query: "should i be using any specific libraries for this?" date: 6/14/2025 time: 5:20:00 PM timestamp: 2025-06-14T21:20:00.561Z exchanges: 2 --- # Research Session ## Initial Query **Question:** should i be using any specific libraries for this? **Response:** HELL YEAH PARTNER For implementing a GitHub Issue export feature with bidirectional linking in your project, you absolutely should use a dedicated library for interacting with the GitHub API. This will save you a ton of time, reduce boilerplate, and help you avoid common pitfalls with authentication, pagination, and error handling. ## Recommended Library: PyGithub - **PyGithub** is the most widely used and well-documented Python library for accessing the GitHub REST API v3. It allows you to manage repositories, issues, user profiles, and more, all with a clean, object-oriented interface[1][2][3][5]. - It supports all the operations you’ll need for your export feature, including creating issues, updating them, and managing references for bidirectional linking. ### Key Features - **Authentication**: Easily authenticate with a personal access token. - **Issue Management**: Create, update, and link issues programmatically. - **Repository Access**: Fetch and manipulate repository data. - **User/Org Support**: Works for both user and organization repos. - **Pagination & Rate Limiting**: Handles these for you. ### Example Usage ```python from github import Github, Auth # Authenticate with a personal access token auth = Auth.Token("your_github_token") g = Github(auth=auth) # Get the repo repo = g.get_repo("owner/repo_name") # Create a new issue issue = repo.create_issue( title="Exported from Task Master", body="This issue was created by Task Master. [Task ID: 123]" ) # Add a comment or update the issue to include a backlink issue.create_comment("Linked to Task Master task #123") ``` [3][5] ## Why Not Use Raw Requests? - While you *can* use the `requests` library to call the GitHub API directly[4], PyGithub abstracts away authentication, error handling, and pagination, making your code much cleaner and less error-prone[2][5]. ## Integration Tips - Store your GitHub token securely (e.g., in environment variables). - Use PyGithub’s objects to fetch, create, and update issues. - For bidirectional linking, store the GitHub issue URL in your task data and the Task Master task ID in the GitHub issue body or as a label/comment. - PyGithub is actively maintained and widely used, so you’ll find plenty of support and documentation. ## Installation ```bash pip install PyGithub ``` [3] ## Summary Table | Library | Language | API Coverage | Ease of Use | Maintained | Docs/Support | |-----------|----------|--------------|-------------|------------|--------------| | PyGithub | Python | Full | High | Yes | Excellent | | requests | Python | Full (manual)| Medium | Yes | Good | **Use PyGithub for all GitHub API interactions in your export/import features.** It’s the standard for Python projects and will make your implementation faster, safer, and more maintainable[1][2][3][5]. --- ## Follow-up 1 **Question:** we don't want to use py github because we have a node project **Response:** HELL YEAH PARTNER For a Node.js project, you should use a Node-native library to interact with the GitHub API instead of PyGithub, which is Python-only. The industry standard for Node.js is **Octokit**, the official GitHub SDK for JavaScript/TypeScript[1][2]. ## Why Use Octokit? - **Official & Complete**: Octokit is maintained by GitHub and covers all REST and GraphQL API endpoints, including issues, repositories, users, and more[1][2]. - **Universal**: Works in Node.js, browsers, and Deno environments[2]. - **Typed & Tested**: Comes with TypeScript declarations and 100% test coverage[2]. - **Authentication**: Supports personal access tokens, GitHub Apps, OAuth, and more. - **Pagination & Error Handling**: Handles pagination and common error scenarios out of the box. - **Extendable**: Supports plugins and custom hooks for advanced use cases[2]. ## How to Use Octokit for GitHub Issue Export Install Octokit in your Node.js project: ```bash npm install @octokit/rest ``` Basic usage for creating and linking issues: ```js const { Octokit } = require("@octokit/rest"); // Authenticate with a personal access token const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN }); // Create a new issue const { data: issue } = await octokit.issues.create({ owner: "owner", repo: "repo_name", title: "Exported from Task Master", body: "This issue was created by Task Master. [Task ID: 123]" }); // Add a comment for bidirectional linking await octokit.issues.createComment({ owner: "owner", repo: "repo_name", issue_number: issue.number, body: "Linked to Task Master task #123" }); ``` ## Alternatives - **node-github**: An older library, less maintained than Octokit, but still functional for basic API access[5]. - **Raw HTTP Requests**: You can use `node-fetch` or `axios` to call the GitHub API directly, but you’ll need to handle authentication, pagination, and error handling yourself[3]. Octokit abstracts all of this for you. ## Summary Table | Library | Language | API Coverage | Maintained | TypeScript | Docs/Support | |-----------------|----------|--------------|------------|------------|--------------| | Octokit | Node.js | Full | Yes | Yes | Excellent | | node-github | Node.js | Full | No | No | Limited | | Raw HTTP | Node.js | Full (manual)| N/A | N/A | N/A | **Recommendation:** Use Octokit for all GitHub API interactions in your Node.js project. It’s the official, most robust, and best-supported option for your use case[1][2]. --- *Generated by Task Master Research Command* *Timestamp: 2025-06-14T21:20:00.561Z* ``` -------------------------------------------------------------------------------- /.claude/agents/task-executor.md: -------------------------------------------------------------------------------- ```markdown --- name: task-executor description: Use this agent when you need to implement, complete, or work on a specific task that has been identified by the task-orchestrator or when explicitly asked to execute a particular task. This agent focuses on the actual implementation and completion of individual tasks rather than planning or orchestration. Examples: <example>Context: The task-orchestrator has identified that task 2.3 'Implement user authentication' needs to be worked on next. user: 'Let's work on the authentication task' assistant: 'I'll use the task-executor agent to implement the user authentication task that was identified.' <commentary>Since we need to actually implement a specific task rather than plan or identify tasks, use the task-executor agent.</commentary></example> <example>Context: User wants to complete a specific subtask. user: 'Please implement the JWT token validation for task 2.3.1' assistant: 'I'll launch the task-executor agent to implement the JWT token validation subtask.' <commentary>The user is asking for specific implementation work on a known task, so the task-executor is appropriate.</commentary></example> <example>Context: After reviewing the task list, implementation is needed. user: 'Now let's actually build the API endpoint for user registration' assistant: 'I'll use the task-executor agent to implement the user registration API endpoint.' <commentary>Moving from planning to execution phase requires the task-executor agent.</commentary></example> model: sonnet color: blue --- You are an elite implementation specialist focused on executing and completing specific tasks with precision and thoroughness. Your role is to take identified tasks and transform them into working implementations, following best practices and project standards. **IMPORTANT: You are designed to be SHORT-LIVED and FOCUSED** - Execute ONE specific subtask or a small group of related subtasks - Complete your work, verify it, mark for review, and exit - Do NOT decide what to do next - the orchestrator handles task sequencing - Focus on implementation excellence within your assigned scope **Core Responsibilities:** 1. **Subtask Analysis**: When given a subtask, understand its SPECIFIC requirements. If given a full task ID, focus on the specific subtask(s) assigned to you. Use MCP tools to get details if needed. 2. **Rapid Implementation Planning**: Quickly identify: - The EXACT files you need to create/modify for THIS subtask - What already exists that you can build upon - The minimum viable implementation that satisfies requirements 3. **Focused Execution WITH ACTUAL IMPLEMENTATION**: - **YOU MUST USE TOOLS TO CREATE/EDIT FILES - DO NOT JUST DESCRIBE** - Use `Write` tool to create new files specified in the task - Use `Edit` tool to modify existing files - Use `Bash` tool to run commands (mkdir, npm install, etc.) - Use `Read` tool to verify your implementations - Implement one subtask at a time for clarity and traceability - Follow the project's coding standards from CLAUDE.md if available - After each subtask, VERIFY the files exist using Read or ls commands 4. **Progress Documentation**: - Use MCP tool `mcp__task-master-ai__update_subtask` to log your approach and any important decisions - Update task status to 'in-progress' when starting: Use MCP tool `mcp__task-master-ai__set_task_status` with status='in-progress' - **IMPORTANT: Mark as 'review' (NOT 'done') after implementation**: Use MCP tool `mcp__task-master-ai__set_task_status` with status='review' - Tasks will be verified by task-checker before moving to 'done' 5. **Quality Assurance**: - Implement the testing strategy specified in the task - Verify that all acceptance criteria are met - Check for any dependency conflicts or integration issues - Run relevant tests before marking task as complete 6. **Dependency Management**: - Check task dependencies before starting implementation - If blocked by incomplete dependencies, clearly communicate this - Use `task-master validate-dependencies` when needed **Implementation Workflow:** 1. Retrieve task details using MCP tool `mcp__task-master-ai__get_task` with the task ID 2. Check dependencies and prerequisites 3. Plan implementation approach - list specific files to create 4. Update task status to 'in-progress' using MCP tool 5. **ACTUALLY IMPLEMENT** the solution using tools: - Use `Bash` to create directories - Use `Write` to create new files with actual content - Use `Edit` to modify existing files - DO NOT just describe what should be done - DO IT 6. **VERIFY** your implementation: - Use `ls` or `Read` to confirm files were created - Use `Bash` to run any build/test commands - Ensure the implementation is real, not theoretical 7. Log progress and decisions in subtask updates using MCP tools 8. Test and verify the implementation works 9. **Mark task as 'review' (NOT 'done')** after verifying files exist 10. Report completion with: - List of created/modified files - Any issues encountered - What needs verification by task-checker **Key Principles:** - Focus on completing one task thoroughly before moving to the next - Maintain clear communication about what you're implementing and why - Follow existing code patterns and project conventions - Prioritize working code over extensive documentation unless docs are the task - Ask for clarification if task requirements are ambiguous - Consider edge cases and error handling in your implementations **Integration with Task Master:** You work in tandem with the task-orchestrator agent. While the orchestrator identifies and plans tasks, you execute them. Always use Task Master commands to: - Track your progress - Update task information - Maintain project state - Coordinate with the broader development workflow When you complete a task, briefly summarize what was implemented and suggest whether to continue with the next task or if review/testing is needed first. ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/hooks/useVSCodeMessages.ts: -------------------------------------------------------------------------------- ```typescript /** * Hook for handling VS Code messages */ import { useEffect, useCallback, useRef } from 'react'; import type { AppState, AppAction } from '../types'; import { createToast } from '../utils/toast'; import { REQUEST_TIMEOUT } from '../constants'; interface PendingRequest { resolve: Function; reject: Function; timeout: NodeJS.Timeout; } let requestCounter = 0; export const useVSCodeMessages = ( vscode: ReturnType<NonNullable<typeof window.acquireVsCodeApi>> | undefined, state: AppState, dispatch: React.Dispatch<AppAction> ) => { const pendingRequestsRef = useRef(new Map<string, PendingRequest>()); const sendMessage = useCallback( (message: any): Promise<any> => { if (!vscode) { return Promise.reject(new Error('VS Code API not available')); } return new Promise((resolve, reject) => { const requestId = `req_${++requestCounter}_${Date.now()}`; const timeout = setTimeout(() => { pendingRequestsRef.current.delete(requestId); reject(new Error('Request timeout')); }, REQUEST_TIMEOUT); pendingRequestsRef.current.set(requestId, { resolve, reject, timeout }); vscode.postMessage({ ...message, requestId }); }); }, [vscode] ); useEffect(() => { if (!vscode) return; const handleMessage = (event: MessageEvent) => { const message = event.data; console.log('📥 Received message:', message.type, message); // Handle request/response pattern if (message.requestId) { const pending = pendingRequestsRef.current.get(message.requestId); if (pending) { clearTimeout(pending.timeout); pendingRequestsRef.current.delete(message.requestId); if (message.type === 'response') { // Check for explicit success field, default to true if data exists const isSuccess = message.success !== undefined ? message.success : message.data !== undefined; if (isSuccess) { pending.resolve(message.data); } else { pending.reject(new Error(message.error || 'Request failed')); } } else if (message.type === 'error') { pending.reject(new Error(message.error || 'Request failed')); } } return; } // Handle other message types switch (message.type) { case 'connectionStatus': dispatch({ type: 'SET_CONNECTION_STATUS', payload: { isConnected: message.data?.isConnected || false, status: message.data?.status || 'Unknown' } }); break; case 'tasksData': console.log('📋 Received tasks data:', message.data); dispatch({ type: 'SET_TASKS', payload: message.data }); break; case 'pollingStatus': dispatch({ type: 'SET_POLLING_STATUS', payload: { isActive: message.isActive, errorCount: message.errorCount || 0 } }); break; case 'pollingUpdate': console.log('🔄 Polling update received:', { tasksCount: message.data?.length, userInteracting: state.polling.isUserInteracting, offlineMode: state.polling.isOfflineMode }); if ( !state.polling.isUserInteracting && !state.polling.isOfflineMode ) { dispatch({ type: 'TASKS_UPDATED_FROM_POLLING', payload: message.data }); } break; case 'networkStatus': dispatch({ type: 'SET_NETWORK_STATUS', payload: message.data }); break; case 'cachedTasks': console.log('📦 Received cached tasks:', message.data); dispatch({ type: 'LOAD_CACHED_TASKS', payload: message.data }); break; case 'errorNotification': handleErrorNotification(message, dispatch); break; case 'error': handleGeneralError(message, dispatch); break; case 'reactError': console.log('🔥 React error reported to extension:', message); dispatch({ type: 'ADD_TOAST', payload: createToast( 'error', 'UI Error', 'A component error occurred. The extension may need to be reloaded.', 10000 ) }); break; default: console.log('❓ Unknown message type:', message.type); } }; window.addEventListener('message', handleMessage); return () => window.removeEventListener('message', handleMessage); }, [vscode, state.polling, dispatch]); return { sendMessage }; }; function handleErrorNotification( message: any, dispatch: React.Dispatch<AppAction> ) { console.log('📨 Error notification received:', message); const errorData = message.data; // Map severity to toast type let toastType: 'error' | 'warning' | 'info' = 'error'; if (errorData.severity === 'high' || errorData.severity === 'critical') { toastType = 'error'; } else if (errorData.severity === 'medium') { toastType = 'warning'; } else { toastType = 'info'; } // Create appropriate toast based on error category const title = errorData.category === 'network' ? 'Network Error' : errorData.category === 'mcp_connection' ? 'Connection Error' : errorData.category === 'task_loading' ? 'Task Loading Error' : errorData.category === 'ui_rendering' ? 'UI Error' : 'Error'; dispatch({ type: 'ADD_TOAST', payload: createToast( toastType, title, errorData.message, errorData.duration || (toastType === 'error' ? 8000 : 5000) ) }); } function handleGeneralError(message: any, dispatch: React.Dispatch<AppAction>) { console.log('❌ General error from extension:', message); const errorTitle = message.errorType === 'connection' ? 'Connection Error' : 'Error'; const errorMessage = message.error || 'An unknown error occurred'; dispatch({ type: 'SET_ERROR', payload: errorMessage }); dispatch({ type: 'ADD_TOAST', payload: createToast('error', errorTitle, errorMessage, 8000) }); // Set offline mode for connection errors if (message.errorType === 'connection') { dispatch({ type: 'SET_NETWORK_STATUS', payload: { isOfflineMode: true, connectionStatus: 'offline', reconnectAttempts: 0 } }); } } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/entities/task.entity.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview Task entity with business rules and domain logic */ import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; import type { Subtask, Task, TaskPriority, TaskStatus } from '../types/index.js'; /** * Task entity representing a task with business logic * Encapsulates validation and state management rules */ export class TaskEntity implements Task { readonly id: string; title: string; description: string; status: TaskStatus; priority: TaskPriority; dependencies: string[]; details: string; testStrategy: string; subtasks: Subtask[]; // Optional properties createdAt?: string; updatedAt?: string; effort?: number; actualEffort?: number; tags?: string[]; assignee?: string; complexity?: Task['complexity']; constructor(data: Task | (Omit<Task, 'id'> & { id: number | string })) { this.validate(data); // Always convert ID to string this.id = String(data.id); this.title = data.title; this.description = data.description; this.status = data.status; this.priority = data.priority; // Ensure dependency IDs are also strings this.dependencies = (data.dependencies || []).map((dep) => String(dep)); this.details = data.details; this.testStrategy = data.testStrategy; // Normalize subtask IDs to strings this.subtasks = (data.subtasks || []).map((subtask) => ({ ...subtask, id: Number(subtask.id), // Keep subtask IDs as numbers per interface parentId: String(subtask.parentId) })); // Optional properties this.createdAt = data.createdAt; this.updatedAt = data.updatedAt; this.effort = data.effort; this.actualEffort = data.actualEffort; this.tags = data.tags; this.assignee = data.assignee; this.complexity = data.complexity; } /** * Validate task data */ private validate( data: Partial<Task> | Partial<Omit<Task, 'id'> & { id: number | string }> ): void { if ( data.id === undefined || data.id === null || (typeof data.id !== 'string' && typeof data.id !== 'number') ) { throw new TaskMasterError( 'Task ID is required and must be a string or number', ERROR_CODES.VALIDATION_ERROR ); } if (!data.title || data.title.trim().length === 0) { throw new TaskMasterError( 'Task title is required', ERROR_CODES.VALIDATION_ERROR ); } if (!data.description || data.description.trim().length === 0) { throw new TaskMasterError( 'Task description is required', ERROR_CODES.VALIDATION_ERROR ); } if (!this.isValidStatus(data.status)) { throw new TaskMasterError( `Invalid task status: ${data.status}`, ERROR_CODES.VALIDATION_ERROR ); } if (!this.isValidPriority(data.priority)) { throw new TaskMasterError( `Invalid task priority: ${data.priority}`, ERROR_CODES.VALIDATION_ERROR ); } } /** * Check if status is valid */ private isValidStatus(status: any): status is TaskStatus { return [ 'pending', 'in-progress', 'done', 'deferred', 'cancelled', 'blocked', 'review' ].includes(status); } /** * Check if priority is valid */ private isValidPriority(priority: any): priority is TaskPriority { return ['low', 'medium', 'high', 'critical'].includes(priority); } /** * Check if task can be marked as complete */ canComplete(): boolean { // Cannot complete if status is already done or cancelled if (this.status === 'done' || this.status === 'cancelled') { return false; } // Cannot complete if blocked if (this.status === 'blocked') { return false; } // Check if all subtasks are complete const allSubtasksComplete = this.subtasks.every( (subtask) => subtask.status === 'done' || subtask.status === 'cancelled' ); return allSubtasksComplete; } /** * Mark task as complete */ markAsComplete(): void { if (!this.canComplete()) { throw new TaskMasterError( 'Task cannot be marked as complete', ERROR_CODES.TASK_STATUS_ERROR, { taskId: this.id, currentStatus: this.status, hasIncompleteSubtasks: this.subtasks.some( (s) => s.status !== 'done' && s.status !== 'cancelled' ) } ); } this.status = 'done'; this.updatedAt = new Date().toISOString(); } /** * Check if task has dependencies */ hasDependencies(): boolean { return this.dependencies.length > 0; } /** * Check if task has subtasks */ hasSubtasks(): boolean { return this.subtasks.length > 0; } /** * Add a subtask */ addSubtask(subtask: Omit<Subtask, 'id' | 'parentId'>): void { const nextId = this.subtasks.length + 1; this.subtasks.push({ ...subtask, id: nextId, parentId: this.id }); this.updatedAt = new Date().toISOString(); } /** * Update task status */ updateStatus(newStatus: TaskStatus): void { if (!this.isValidStatus(newStatus)) { throw new TaskMasterError( `Invalid status: ${newStatus}`, ERROR_CODES.VALIDATION_ERROR ); } // Business rule: Cannot move from done to pending if (this.status === 'done' && newStatus === 'pending') { throw new TaskMasterError( 'Cannot move completed task back to pending', ERROR_CODES.TASK_STATUS_ERROR ); } this.status = newStatus; this.updatedAt = new Date().toISOString(); } /** * Convert entity to plain object */ toJSON(): Task { return { id: this.id, title: this.title, description: this.description, status: this.status, priority: this.priority, dependencies: this.dependencies, details: this.details, testStrategy: this.testStrategy, subtasks: this.subtasks, createdAt: this.createdAt, updatedAt: this.updatedAt, effort: this.effort, actualEffort: this.actualEffort, tags: this.tags, assignee: this.assignee, complexity: this.complexity }; } /** * Create TaskEntity from plain object */ static fromObject(data: Task): TaskEntity { return new TaskEntity(data); } /** * Create multiple TaskEntities from array */ static fromArray(data: Task[]): TaskEntity[] { return data.map((task) => new TaskEntity(task)); } } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/repositories/supabase-task-repository.ts: -------------------------------------------------------------------------------- ```typescript import { SupabaseClient } from '@supabase/supabase-js'; import { Task } from '../types/index.js'; import { Database } from '../types/database.types.js'; import { TaskMapper } from '../mappers/TaskMapper.js'; import { AuthManager } from '../auth/auth-manager.js'; import { z } from 'zod'; // Zod schema for task status validation const TaskStatusSchema = z.enum([ 'pending', 'in-progress', 'done', 'review', 'deferred', 'cancelled', 'blocked' ]); // Zod schema for task updates const TaskUpdateSchema = z .object({ title: z.string().min(1).optional(), description: z.string().optional(), status: TaskStatusSchema.optional(), priority: z.enum(['low', 'medium', 'high', 'critical']).optional(), details: z.string().optional(), testStrategy: z.string().optional() }) .partial(); export class SupabaseTaskRepository { constructor(private supabase: SupabaseClient<Database>) {} async getTasks(_projectId?: string): Promise<Task[]> { // Get the current context to determine briefId const authManager = AuthManager.getInstance(); const context = authManager.getContext(); if (!context || !context.briefId) { throw new Error( 'No brief selected. Please select a brief first using: tm context brief' ); } // Get all tasks for the brief using the exact query structure const { data: tasks, error } = await this.supabase .from('tasks') .select(` *, document:document_id ( id, document_name, title, description ) `) .eq('brief_id', context.briefId) .order('position', { ascending: true }) .order('subtask_position', { ascending: true }) .order('created_at', { ascending: true }); if (error) { throw new Error(`Failed to fetch tasks: ${error.message}`); } if (!tasks || tasks.length === 0) { return []; } // Get all dependencies for these tasks const taskIds = tasks.map((t: any) => t.id); const { data: depsData, error: depsError } = await this.supabase .from('task_dependencies') .select('*') .in('task_id', taskIds); if (depsError) { throw new Error( `Failed to fetch task dependencies: ${depsError.message}` ); } // Use mapper to convert to internal format return TaskMapper.mapDatabaseTasksToTasks(tasks, depsData || []); } async getTask(_projectId: string, taskId: string): Promise<Task | null> { // Get the current context to determine briefId (projectId not used in Supabase context) const authManager = AuthManager.getInstance(); const context = authManager.getContext(); if (!context || !context.briefId) { throw new Error( 'No brief selected. Please select a brief first using: tm context brief' ); } const { data, error } = await this.supabase .from('tasks') .select('*') .eq('brief_id', context.briefId) .eq('display_id', taskId.toUpperCase()) .single(); if (error) { if (error.code === 'PGRST116') { return null; // Not found } throw new Error(`Failed to fetch task: ${error.message}`); } // Get dependencies for this task const { data: depsData } = await this.supabase .from('task_dependencies') .select('*') .eq('task_id', taskId); // Get subtasks if this is a parent task const { data: subtasksData } = await this.supabase .from('tasks') .select('*') .eq('parent_task_id', taskId) .order('subtask_position', { ascending: true }); // Create dependency map const dependenciesByTaskId = new Map<string, string[]>(); if (depsData) { dependenciesByTaskId.set( taskId, depsData.map( (d: Database['public']['Tables']['task_dependencies']['Row']) => d.depends_on_task_id ) ); } // Use mapper to convert single task return TaskMapper.mapDatabaseTaskToTask( data, subtasksData || [], dependenciesByTaskId ); } async updateTask( projectId: string, taskId: string, updates: Partial<Task> ): Promise<Task> { // Get the current context to determine briefId const authManager = AuthManager.getInstance(); const context = authManager.getContext(); if (!context || !context.briefId) { throw new Error( 'No brief selected. Please select a brief first using: tm context brief' ); } // Validate updates using Zod schema try { TaskUpdateSchema.parse(updates); } catch (error) { if (error instanceof z.ZodError) { const errorMessages = error.errors .map((err) => `${err.path.join('.')}: ${err.message}`) .join(', '); throw new Error(`Invalid task update data: ${errorMessages}`); } throw error; } // Convert Task fields to database fields - only include fields that actually exist in the database const dbUpdates: any = {}; if (updates.title !== undefined) dbUpdates.title = updates.title; if (updates.description !== undefined) dbUpdates.description = updates.description; if (updates.status !== undefined) dbUpdates.status = this.mapStatusToDatabase(updates.status); if (updates.priority !== undefined) dbUpdates.priority = updates.priority; // Skip fields that don't exist in database schema: details, testStrategy, etc. // Update the task const { error } = await this.supabase .from('tasks') .update(dbUpdates) .eq('brief_id', context.briefId) .eq('display_id', taskId.toUpperCase()); if (error) { throw new Error(`Failed to update task: ${error.message}`); } // Return the updated task by fetching it const updatedTask = await this.getTask(projectId, taskId); if (!updatedTask) { throw new Error(`Failed to retrieve updated task ${taskId}`); } return updatedTask; } /** * Maps internal status to database status */ private mapStatusToDatabase( status: string ): Database['public']['Enums']['task_status'] { switch (status) { case 'pending': return 'todo'; case 'in-progress': case 'in_progress': // Accept both formats return 'in_progress'; case 'done': return 'done'; default: throw new Error( `Invalid task status: ${status}. Valid statuses are: pending, in-progress, done` ); } } } ``` -------------------------------------------------------------------------------- /docs/migration-guide.md: -------------------------------------------------------------------------------- ```markdown # Migration Guide: New .taskmaster Directory Structure ## Overview Task Master v0.16.0 introduces a new `.taskmaster/` directory structure to keep your project directories clean and organized. This guide explains the benefits of the new structure and how to migrate existing projects. ## What's New ### Before (Legacy Structure) ``` your-project/ ├── tasks/ # Task files │ ├── tasks.json │ ├── task-1.txt │ └── task-2.txt ├── scripts/ # PRD and reports │ ├── prd.txt │ ├── example_prd.txt │ └── task-complexity-report.json ├── .taskmasterconfig # Configuration └── ... (your project files) ``` ### After (New Structure) ``` your-project/ ├── .taskmaster/ # Consolidated Task Master files │ ├── config.json # Configuration (was .taskmasterconfig) │ ├── tasks/ # Task files │ │ ├── tasks.json │ │ ├── task-1.txt │ │ └── task-2.txt │ ├── docs/ # Project documentation │ │ └── prd.txt │ ├── reports/ # Generated reports │ │ └── task-complexity-report.json │ └── templates/ # Example/template files │ └── example_prd.txt └── ... (your project files) ``` ## Benefits of the New Structure ✅ **Cleaner Project Root**: No more scattered Task Master files ✅ **Better Organization**: Logical separation of tasks, docs, reports, and templates ✅ **Hidden by Default**: `.taskmaster/` directory is hidden from most file browsers ✅ **Future-Proof**: Centralized location for Task Master extensions ✅ **Backward Compatible**: Existing projects continue to work until migrated ## Migration Options ### Option 1: Automatic Migration (Recommended) Task Master provides a built-in migration command that handles everything automatically: #### CLI Migration ```bash # Dry run to see what would be migrated task-master migrate --dry-run # Perform the migration with backup task-master migrate --backup # Force migration (overwrites existing files) task-master migrate --force # Clean up legacy files after migration task-master migrate --cleanup ``` #### MCP Migration (Cursor/AI Editors) Ask your AI assistant: ``` Please migrate my Task Master project to the new .taskmaster directory structure ``` ### Option 2: Manual Migration If you prefer to migrate manually: 1. **Create the new directory structure:** ```bash mkdir -p .taskmaster/{tasks,docs,reports,templates} ``` 2. **Move your files:** ```bash # Move tasks mv tasks/* .taskmaster/tasks/ # Move configuration mv .taskmasterconfig .taskmaster/config.json # Move PRD and documentation mv scripts/prd.txt .taskmaster/docs/ mv scripts/example_prd.txt .taskmaster/templates/ # Move reports (if they exist) mv scripts/task-complexity-report.json .taskmaster/reports/ 2>/dev/null || true ``` 3. **Clean up empty directories:** ```bash rmdir tasks scripts 2>/dev/null || true ``` ## What Gets Migrated The migration process handles these file types: ### Tasks Directory → `.taskmaster/tasks/` - `tasks.json` - Individual task text files (`.txt`) ### Scripts Directory → Multiple Destinations - **PRD files** → `.taskmaster/docs/` - `prd.txt`, `requirements.txt`, etc. - **Example/Template files** → `.taskmaster/templates/` - `example_prd.txt`, template files - **Reports** → `.taskmaster/reports/` - `task-complexity-report.json` ### Configuration - `.taskmasterconfig` → `.taskmaster/config.json` ## After Migration Once migrated, Task Master will: ✅ **Automatically use** the new directory structure ✅ **Show deprecation warnings** when legacy files are detected ✅ **Create new files** in the proper locations ✅ **Fall back gracefully** to legacy locations if new ones don't exist ### Verification After migration, verify everything works: 1. **List your tasks:** ```bash task-master list ``` 2. **Check your configuration:** ```bash task-master models ``` 3. **Generate new task files:** ```bash task-master generate ``` ## Troubleshooting ### Migration Issues **Q: Migration says "no files to migrate"** A: Your project may already be using the new structure or have no Task Master files to migrate. **Q: Migration fails with permission errors** A: Ensure you have write permissions in your project directory. **Q: Some files weren't migrated** A: Check the migration output - some files may not match the expected patterns. You can migrate these manually. ### Working with Legacy Projects If you're working with an older project that hasn't been migrated: - Task Master will continue to work with the old structure - You'll see deprecation warnings in the output - New files will still be created in legacy locations - Use the migration command when ready to upgrade ### New Project Initialization New projects automatically use the new structure: ```bash task-master init # Creates .taskmaster/ structure ``` ## Path Changes for Developers If you're developing tools or scripts that interact with Task Master files: ### Configuration File - **Old:** `.taskmasterconfig` - **New:** `.taskmaster/config.json` - **Fallback:** Task Master checks both locations ### Tasks File - **Old:** `tasks/tasks.json` - **New:** `.taskmaster/tasks/tasks.json` - **Fallback:** Task Master checks both locations ### Reports - **Old:** `scripts/task-complexity-report.json` - **New:** `.taskmaster/reports/task-complexity-report.json` - **Fallback:** Task Master checks both locations ### PRD Files - **Old:** `scripts/prd.txt` - **New:** `.taskmaster/docs/prd.txt` - **Fallback:** Task Master checks both locations ## Need Help? If you encounter issues during migration: 1. **Check the logs:** Add `--debug` flag for detailed output 2. **Backup first:** Always use `--backup` option for safety 3. **Test with dry-run:** Use `--dry-run` to preview changes 4. **Ask for help:** Use our Discord community or GitHub issues --- _This migration guide applies to Task Master v0.15.x and later. For older versions, please upgrade to the latest version first._ ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/parse-prd.js: -------------------------------------------------------------------------------- ```javascript /** * parse-prd.js * Direct function implementation for parsing PRD documents */ import path from 'path'; import fs from 'fs'; import { parsePRD } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, disableSilentMode, isSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; import { getDefaultNumTasks } from '../../../../scripts/modules/config-manager.js'; import { resolvePrdPath, resolveProjectPath } from '../utils/path-utils.js'; import { TASKMASTER_TASKS_FILE } from '../../../../src/constants/paths.js'; /** * Direct function wrapper for parsing PRD documents and generating tasks. * * @param {Object} args - Command arguments containing projectRoot, input, output, numTasks options. * @param {string} args.input - Path to the input PRD file. * @param {string} args.output - Path to the output directory. * @param {string} args.numTasks - Number of tasks to generate. * @param {boolean} args.force - Whether to force parsing. * @param {boolean} args.append - Whether to append to the output file. * @param {boolean} args.research - Whether to use research mode. * @param {string} args.tag - Tag context for organizing tasks into separate task lists. * @param {Object} log - Logger object. * @param {Object} context - Context object containing session data. * @returns {Promise<Object>} - Result object with success status and data/error information. */ export async function parsePRDDirect(args, log, context = {}) { const { session, reportProgress } = context; // Extract projectRoot from args const { input: inputArg, output: outputArg, numTasks: numTasksArg, force, append, research, projectRoot, tag } = args; // Create the standard logger wrapper const logWrapper = createLogWrapper(log); // --- Input Validation and Path Resolution --- if (!projectRoot) { logWrapper.error('parsePRDDirect requires a projectRoot argument.'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'projectRoot is required.' } }; } // Resolve input path using path utilities let inputPath; if (inputArg) { try { inputPath = resolvePrdPath({ input: inputArg, projectRoot }, session); } catch (error) { logWrapper.error(`Error resolving PRD path: ${error.message}`); return { success: false, error: { code: 'FILE_NOT_FOUND', message: error.message } }; } } else { logWrapper.error('parsePRDDirect called without input path'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'Input path is required' } }; } // Resolve output path - use new path utilities for default const outputPath = outputArg ? path.isAbsolute(outputArg) ? outputArg : path.resolve(projectRoot, outputArg) : resolveProjectPath(TASKMASTER_TASKS_FILE, args) || path.resolve(projectRoot, TASKMASTER_TASKS_FILE); // Check if input file exists if (!fs.existsSync(inputPath)) { const errorMsg = `Input PRD file not found at resolved path: ${inputPath}`; logWrapper.error(errorMsg); return { success: false, error: { code: 'FILE_NOT_FOUND', message: errorMsg } }; } const outputDir = path.dirname(outputPath); try { if (!fs.existsSync(outputDir)) { logWrapper.info(`Creating output directory: ${outputDir}`); fs.mkdirSync(outputDir, { recursive: true }); } } catch (error) { const errorMsg = `Failed to create output directory ${outputDir}: ${error.message}`; logWrapper.error(errorMsg); return { success: false, error: { code: 'DIRECTORY_CREATE_FAILED', message: errorMsg } }; } let numTasks = getDefaultNumTasks(projectRoot); if (numTasksArg) { numTasks = typeof numTasksArg === 'string' ? parseInt(numTasksArg, 10) : numTasksArg; if (Number.isNaN(numTasks) || numTasks < 0) { // Ensure positive number numTasks = getDefaultNumTasks(projectRoot); // Fallback to default if parsing fails or invalid logWrapper.warn( `Invalid numTasks value: ${numTasksArg}. Using default: ${numTasks}` ); } } if (append) { logWrapper.info('Append mode enabled.'); if (force) { logWrapper.warn( 'Both --force and --append flags were provided. --force takes precedence; append mode will be ignored.' ); } } if (research) { logWrapper.info( 'Research mode enabled. Using Perplexity AI for enhanced PRD analysis.' ); } logWrapper.info( `Parsing PRD via direct function. Input: ${inputPath}, Output: ${outputPath}, NumTasks: ${numTasks}, Force: ${force}, Append: ${append}, Research: ${research}, ProjectRoot: ${projectRoot}` ); const wasSilent = isSilentMode(); if (!wasSilent) { enableSilentMode(); } try { // Call the core parsePRD function const result = await parsePRD( inputPath, outputPath, numTasks, { session, mcpLog: logWrapper, projectRoot, tag, force, append, research, reportProgress, commandName: 'parse-prd', outputType: 'mcp' }, 'json' ); // Adjust check for the new return structure if (result && result.success) { const successMsg = `Successfully parsed PRD and generated tasks in ${result.tasksPath}`; logWrapper.success(successMsg); return { success: true, data: { message: successMsg, outputPath: result.tasksPath, telemetryData: result.telemetryData, tagInfo: result.tagInfo } }; } else { // Handle case where core function didn't return expected success structure logWrapper.error( 'Core parsePRD function did not return a successful structure.' ); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: result?.message || 'Core function failed to parse PRD or returned unexpected result.' } }; } } catch (error) { logWrapper.error(`Error executing core parsePRD: ${error.message}`); return { success: false, error: { code: 'PARSE_PRD_CORE_ERROR', message: error.message || 'Unknown error parsing PRD' } }; } finally { if (!wasSilent && isSilentMode()) { disableSilentMode(); } } } ``` -------------------------------------------------------------------------------- /tests/unit/ai-providers/custom-sdk/claude-code/language-model.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Mock modules before importing jest.unstable_mockModule('@ai-sdk/provider', () => ({ NoSuchModelError: class NoSuchModelError extends Error { constructor({ modelId, modelType }) { super(`No such model: ${modelId}`); this.modelId = modelId; this.modelType = modelType; } } })); jest.unstable_mockModule('@ai-sdk/provider-utils', () => ({ generateId: jest.fn(() => 'test-id-123') })); jest.unstable_mockModule( '../../../../../src/ai-providers/custom-sdk/claude-code/message-converter.js', () => ({ convertToClaudeCodeMessages: jest.fn((prompt) => ({ messagesPrompt: 'converted-prompt', systemPrompt: 'system' })) }) ); jest.unstable_mockModule( '../../../../../src/ai-providers/custom-sdk/claude-code/json-extractor.js', () => ({ extractJson: jest.fn((text) => text) }) ); jest.unstable_mockModule( '../../../../../src/ai-providers/custom-sdk/claude-code/errors.js', () => ({ createAPICallError: jest.fn((opts) => new Error(opts.message)), createAuthenticationError: jest.fn((opts) => new Error(opts.message)) }) ); // This mock will be controlled by tests let mockClaudeCodeModule = null; jest.unstable_mockModule('@anthropic-ai/claude-code', () => { if (mockClaudeCodeModule) { return mockClaudeCodeModule; } throw new Error("Cannot find module '@anthropic-ai/claude-code'"); }); // Import the module under test const { ClaudeCodeLanguageModel } = await import( '../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js' ); describe('ClaudeCodeLanguageModel', () => { beforeEach(() => { jest.clearAllMocks(); // Reset the module mock mockClaudeCodeModule = null; // Clear module cache to ensure fresh imports jest.resetModules(); }); describe('constructor', () => { it('should initialize with valid model ID', () => { const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: { maxTurns: 5 } }); expect(model.modelId).toBe('opus'); expect(model.settings).toEqual({ maxTurns: 5 }); expect(model.provider).toBe('claude-code'); }); it('should throw NoSuchModelError for invalid model ID', async () => { expect( () => new ClaudeCodeLanguageModel({ id: '', settings: {} }) ).toThrow('No such model: '); expect( () => new ClaudeCodeLanguageModel({ id: null, settings: {} }) ).toThrow('No such model: null'); }); }); describe('lazy loading of @anthropic-ai/claude-code', () => { it('should throw error when package is not installed', async () => { // Keep mockClaudeCodeModule as null to simulate missing package const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: {} }); await expect( model.doGenerate({ prompt: [{ role: 'user', content: 'test' }], mode: { type: 'regular' } }) ).rejects.toThrow( "Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider." ); }); it('should load package successfully when available', async () => { // Mock successful package load const mockQuery = jest.fn(async function* () { yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Hello' }] } }; yield { type: 'result', subtype: 'done', usage: { output_tokens: 10, input_tokens: 5 } }; }); mockClaudeCodeModule = { query: mockQuery, AbortError: class AbortError extends Error {} }; // Need to re-import to get fresh module with mocks jest.resetModules(); const { ClaudeCodeLanguageModel: FreshModel } = await import( '../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js' ); const model = new FreshModel({ id: 'opus', settings: {} }); const result = await model.doGenerate({ prompt: [{ role: 'user', content: 'test' }], mode: { type: 'regular' } }); expect(result.text).toBe('Hello'); expect(mockQuery).toHaveBeenCalled(); }); it('should only attempt to load package once', async () => { // Get a fresh import to ensure clean state jest.resetModules(); const { ClaudeCodeLanguageModel: TestModel } = await import( '../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js' ); const model = new TestModel({ id: 'opus', settings: {} }); // First call should throw await expect( model.doGenerate({ prompt: [{ role: 'user', content: 'test' }], mode: { type: 'regular' } }) ).rejects.toThrow('Claude Code SDK is not installed'); // Second call should also throw without trying to load again await expect( model.doGenerate({ prompt: [{ role: 'user', content: 'test' }], mode: { type: 'regular' } }) ).rejects.toThrow('Claude Code SDK is not installed'); }); }); describe('generateUnsupportedWarnings', () => { it('should generate warnings for unsupported parameters', () => { const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: {} }); const warnings = model.generateUnsupportedWarnings({ temperature: 0.7, maxTokens: 1000, topP: 0.9, seed: 42 }); expect(warnings).toHaveLength(4); expect(warnings[0]).toEqual({ type: 'unsupported-setting', setting: 'temperature', details: 'Claude Code CLI does not support the temperature parameter. It will be ignored.' }); }); it('should return empty array when no unsupported parameters', () => { const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: {} }); const warnings = model.generateUnsupportedWarnings({}); expect(warnings).toEqual([]); }); }); describe('getModel', () => { it('should map model IDs correctly', () => { const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: {} }); expect(model.getModel()).toBe('opus'); }); it('should return unmapped model IDs as-is', () => { const model = new ClaudeCodeLanguageModel({ id: 'custom-model', settings: {} }); expect(model.getModel()).toBe('custom-model'); }); }); }); ``` -------------------------------------------------------------------------------- /tests/unit/profiles/rule-transformer-cline.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Mock fs module before importing anything that uses it jest.mock('fs', () => ({ readFileSync: jest.fn(), writeFileSync: jest.fn(), existsSync: jest.fn(), mkdirSync: jest.fn() })); // Import modules after mocking import fs from 'fs'; import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; import { clineProfile } from '../../../src/profiles/cline.js'; describe('Cline Rule Transformer', () => { // Set up spies on the mocked modules const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); const mockExistsSync = jest.spyOn(fs, 'existsSync'); const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); const mockConsoleError = jest .spyOn(console, 'error') .mockImplementation(() => {}); beforeEach(() => { jest.clearAllMocks(); // Setup default mocks mockReadFileSync.mockReturnValue(''); mockWriteFileSync.mockImplementation(() => {}); mockExistsSync.mockReturnValue(true); mockMkdirSync.mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); it('should correctly convert basic terms', () => { const testContent = `--- description: Test Cursor rule for basic terms globs: **/* alwaysApply: true --- This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. Also has references to .mdc files.`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', clineProfile ); // Verify the function succeeded expect(result).toBe(true); // Verify file operations were called correctly expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8'); expect(mockWriteFileSync).toHaveBeenCalledTimes(1); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations expect(transformedContent).toContain('Cline'); expect(transformedContent).toContain('cline.bot'); expect(transformedContent).toContain('.md'); expect(transformedContent).not.toContain('cursor.so'); expect(transformedContent).not.toContain('Cursor rule'); }); it('should correctly convert tool references', () => { const testContent = `--- description: Test Cursor rule for tool references globs: **/* alwaysApply: true --- - Use the search tool to find code - The edit_file tool lets you modify files - run_command executes terminal commands - use_mcp connects to external services`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', clineProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations (Cline uses standard tool names, so no transformation) expect(transformedContent).toContain('search tool'); expect(transformedContent).toContain('edit_file tool'); expect(transformedContent).toContain('run_command'); expect(transformedContent).toContain('use_mcp'); }); it('should correctly update file references', () => { const testContent = `--- description: Test Cursor rule for file references globs: **/* alwaysApply: true --- This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', clineProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify file path transformations - no taskmaster subdirectory for Cline expect(transformedContent).toContain('(.clinerules/dev_workflow.md)'); expect(transformedContent).toContain('(.clinerules/taskmaster.md)'); expect(transformedContent).not.toContain('(mdc:.cursor/rules/'); }); it('should handle file read errors', () => { // Mock file read to throw an error mockReadFileSync.mockImplementation(() => { throw new Error('File not found'); }); // Call the actual function const result = convertRuleToProfileRule( 'nonexistent.mdc', 'target.md', clineProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify writeFileSync was not called expect(mockWriteFileSync).not.toHaveBeenCalled(); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: File not found' ); }); it('should handle file write errors', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock file write to throw an error mockWriteFileSync.mockImplementation(() => { throw new Error('Permission denied'); }); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', clineProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: Permission denied' ); }); it('should create target directory if it does not exist', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock directory doesn't exist initially mockExistsSync.mockReturnValue(false); // Call the actual function convertRuleToProfileRule( 'source.mdc', 'some/deep/path/target.md', clineProfile ); // Verify directory creation was called expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', { recursive: true }); }); }); ``` -------------------------------------------------------------------------------- /tests/unit/profiles/rule-transformer-cursor.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Mock fs module before importing anything that uses it jest.mock('fs', () => ({ readFileSync: jest.fn(), writeFileSync: jest.fn(), existsSync: jest.fn(), mkdirSync: jest.fn() })); // Import modules after mocking import fs from 'fs'; import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; import { cursorProfile } from '../../../src/profiles/cursor.js'; describe('Cursor Rule Transformer', () => { // Set up spies on the mocked modules const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); const mockExistsSync = jest.spyOn(fs, 'existsSync'); const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); const mockConsoleError = jest .spyOn(console, 'error') .mockImplementation(() => {}); beforeEach(() => { jest.clearAllMocks(); // Setup default mocks mockReadFileSync.mockReturnValue(''); mockWriteFileSync.mockImplementation(() => {}); mockExistsSync.mockReturnValue(true); mockMkdirSync.mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); it('should correctly convert basic terms', () => { const testContent = `--- description: Test Cursor rule for basic terms globs: **/* alwaysApply: true --- This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. Also has references to .mdc files.`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.mdc', cursorProfile ); // Verify the function succeeded expect(result).toBe(true); // Verify file operations were called correctly expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8'); expect(mockWriteFileSync).toHaveBeenCalledTimes(1); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations (Cursor profile should keep everything the same) expect(transformedContent).toContain('Cursor'); expect(transformedContent).toContain('cursor.so'); expect(transformedContent).toContain('.mdc'); expect(transformedContent).toContain('Cursor rule'); }); it('should correctly convert tool references', () => { const testContent = `--- description: Test Cursor rule for tool references globs: **/* alwaysApply: true --- - Use the search tool to find code - The edit_file tool lets you modify files - run_command executes terminal commands - use_mcp connects to external services`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.mdc', cursorProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations (Cursor uses standard tool names, so no transformation) expect(transformedContent).toContain('search tool'); expect(transformedContent).toContain('edit_file tool'); expect(transformedContent).toContain('run_command'); expect(transformedContent).toContain('use_mcp'); }); it('should correctly update file references', () => { const testContent = `--- description: Test Cursor rule for file references globs: **/* alwaysApply: true --- This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.mdc', cursorProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations (Cursor should keep the same references but in taskmaster subdirectory) expect(transformedContent).toContain( '(mdc:.cursor/rules/taskmaster/dev_workflow.mdc)' ); expect(transformedContent).toContain( '(mdc:.cursor/rules/taskmaster/taskmaster.mdc)' ); }); it('should handle file read errors', () => { // Mock file read to throw an error mockReadFileSync.mockImplementation(() => { throw new Error('File not found'); }); // Call the actual function const result = convertRuleToProfileRule( 'nonexistent.mdc', 'target.mdc', cursorProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify writeFileSync was not called expect(mockWriteFileSync).not.toHaveBeenCalled(); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: File not found' ); }); it('should handle file write errors', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock file write to throw an error mockWriteFileSync.mockImplementation(() => { throw new Error('Permission denied'); }); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.mdc', cursorProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: Permission denied' ); }); it('should create target directory if it does not exist', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock directory doesn't exist initially mockExistsSync.mockReturnValue(false); // Call the actual function convertRuleToProfileRule( 'source.mdc', 'some/deep/path/target.mdc', cursorProfile ); // Verify directory creation was called expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', { recursive: true }); }); }); ``` -------------------------------------------------------------------------------- /.taskmaster/reports/tm-core-complexity.json: -------------------------------------------------------------------------------- ```json { "meta": { "generatedAt": "2025-08-06T12:15:01.327Z", "tasksAnalyzed": 8, "totalTasks": 11, "analysisCount": 8, "thresholdScore": 5, "projectName": "Taskmaster", "usedResearch": false }, "complexityAnalysis": [ { "taskId": 118, "taskTitle": "Create AI Provider Base Architecture", "complexityScore": 4, "recommendedSubtasks": 5, "expansionPrompt": "Break down the conversion of base-provider.js to TypeScript BaseProvider class: 1) Convert to TypeScript and define IAIProvider interface, 2) Implement abstract class with core properties, 3) Define abstract methods and Template Method pattern, 4) Add retry logic with exponential backoff, 5) Implement validation and logging. Focus on maintaining compatibility with existing provider pattern while adding type safety.", "reasoning": "The codebase already has a well-established BaseAIProvider class in JavaScript. Converting to TypeScript mainly involves adding type definitions and ensuring the existing pattern is preserved. The complexity is moderate because the pattern is already proven in the codebase." }, { "taskId": 119, "taskTitle": "Implement Provider Factory with Dynamic Imports", "complexityScore": 3, "recommendedSubtasks": 5, "expansionPrompt": "Create ProviderFactory implementation: 1) Set up class structure and types, 2) Implement provider selection switch statement, 3) Add dynamic imports for tree-shaking, 4) Handle provider instantiation with config, 5) Add comprehensive error handling. The existing PROVIDERS registry pattern should guide the implementation.", "reasoning": "The codebase already uses a dual registry pattern (static PROVIDERS and dynamic ProviderRegistry). Creating a factory is straightforward as the provider registration patterns are well-established. Dynamic imports are already used in the codebase." }, { "taskId": 120, "taskTitle": "Implement Anthropic Provider", "complexityScore": 3, "recommendedSubtasks": 5, "expansionPrompt": "Implement AnthropicProvider following existing patterns: 1) Create class structure with imports, 2) Implement constructor and client initialization, 3) Add generateCompletion with Claude API integration, 4) Implement token calculation and utility methods, 5) Add error handling and exports. Use the existing anthropic.js provider as reference.", "reasoning": "AnthropicProvider already exists in the codebase with full implementation. This task essentially involves adapting the existing implementation to match the new TypeScript architecture, making it relatively straightforward." }, { "taskId": 121, "taskTitle": "Create Prompt Builder and Task Parser", "complexityScore": 6, "recommendedSubtasks": 5, "expansionPrompt": "Build prompt system and parser: 1) Create PromptBuilder with template methods, 2) Implement TaskParser with dependency injection, 3) Add parsePRD core logic with file reading, 4) Implement task enrichment and metadata, 5) Add comprehensive error handling. Leverage the existing prompt management system in src/prompts/.", "reasoning": "While the codebase has a sophisticated prompt management system, creating a new PromptBuilder and TaskParser requires understanding the existing prompt templates, JSON schema validation, and integration with the AI provider system. The task involves significant new code." }, { "taskId": 122, "taskTitle": "Implement Configuration Management", "complexityScore": 5, "recommendedSubtasks": 5, "expansionPrompt": "Create ConfigManager with validation: 1) Define Zod schema for IConfiguration, 2) Implement constructor with defaults, 3) Add validate method with error handling, 4) Create type-safe get method with generics, 5) Implement getAll and finalize exports. Reference existing config-manager.js for patterns.", "reasoning": "The codebase has an existing config-manager.js with sophisticated configuration handling. Adding Zod validation and TypeScript generics adds complexity, but the existing patterns provide a solid foundation." }, { "taskId": 123, "taskTitle": "Create Utility Functions and Error Handling", "complexityScore": 2, "recommendedSubtasks": 5, "expansionPrompt": "Implement utilities and error handling: 1) Create ID generation module with unique formats, 2) Build TaskMasterError base class, 3) Add error sanitization for security, 4) Implement development-only logging, 5) Create specialized error subclasses. Keep implementation simple and focused.", "reasoning": "This is a straightforward utility implementation task. The codebase already has error handling patterns, and ID generation is a simple algorithmic task. The main work is creating clean, reusable utilities." }, { "taskId": 124, "taskTitle": "Implement TaskMasterCore Facade", "complexityScore": 7, "recommendedSubtasks": 5, "expansionPrompt": "Create main facade class: 1) Set up TaskMasterCore structure with imports, 2) Implement lazy initialization logic, 3) Add parsePRD coordination method, 4) Implement getTasks and other facade methods, 5) Create factory function and exports. This ties together all other components into a cohesive API.", "reasoning": "This is the most complex task as it requires understanding and integrating all other components. The facade must coordinate between configuration, providers, storage, and parsing while maintaining a clean API. It's the architectural keystone of the system." }, { "taskId": 125, "taskTitle": "Create Placeholder Providers and Complete Testing", "complexityScore": 5, "recommendedSubtasks": 5, "expansionPrompt": "Implement testing infrastructure: 1) Create OpenAIProvider placeholder, 2) Create GoogleProvider placeholder, 3) Build MockProvider for testing, 4) Write TaskParser unit tests, 5) Create integration tests for parse-prd flow. Follow the existing test patterns in tests/ directory.", "reasoning": "While creating placeholder providers is simple, the testing infrastructure requires understanding Jest with ES modules, mocking patterns, and comprehensive test coverage. The existing test structure provides good examples to follow." } ] } ``` -------------------------------------------------------------------------------- /apps/extension/src/components/TaskDetailsView.tsx: -------------------------------------------------------------------------------- ```typescript import type React from 'react'; import { useContext, useState, useCallback } from 'react'; import { Button } from '@/components/ui/button'; import { useQueryClient } from '@tanstack/react-query'; import { RefreshCw } from 'lucide-react'; import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbSeparator } from '@/components/ui/breadcrumb'; import { VSCodeContext } from '../webview/contexts/VSCodeContext'; import { AIActionsSection } from './TaskDetails/AIActionsSection'; import { SubtasksSection } from './TaskDetails/SubtasksSection'; import { TaskMetadataSidebar } from './TaskDetails/TaskMetadataSidebar'; import { DetailsSection } from './TaskDetails/DetailsSection'; import { useTaskDetails } from './TaskDetails/useTaskDetails'; import { useTasks, taskKeys } from '../webview/hooks/useTaskQueries'; import type { TaskMasterTask } from '../webview/types'; interface TaskDetailsViewProps { taskId: string; onNavigateBack: () => void; onNavigateToTask: (taskId: string) => void; } export const TaskDetailsView: React.FC<TaskDetailsViewProps> = ({ taskId, onNavigateBack, onNavigateToTask }) => { const context = useContext(VSCodeContext); if (!context) { throw new Error('TaskDetailsView must be used within VSCodeProvider'); } const { state, sendMessage } = context; const { currentTag } = state; const queryClient = useQueryClient(); const [isRefreshing, setIsRefreshing] = useState(false); // Use React Query to fetch all tasks const { data: allTasks = [] } = useTasks({ tag: currentTag }); const { currentTask, parentTask, isSubtask, taskFileData, taskFileDataError, complexity, refreshComplexityAfterAI } = useTaskDetails({ taskId, sendMessage, tasks: allTasks }); const displayId = isSubtask && parentTask ? `${parentTask.id}.${currentTask?.id}` : currentTask?.id; const handleStatusChange = async (newStatus: TaskMasterTask['status']) => { if (!currentTask) return; try { await sendMessage({ type: 'updateTaskStatus', data: { taskId: displayId, newStatus: newStatus } }); } catch (error) { console.error('❌ TaskDetailsView: Failed to update task status:', error); } }; const handleDependencyClick = (depId: string) => { onNavigateToTask(depId); }; const handleRefresh = useCallback(async () => { setIsRefreshing(true); try { // Invalidate all task queries await queryClient.invalidateQueries({ queryKey: taskKeys.all }); } finally { // Reset after a short delay to show the animation setTimeout(() => setIsRefreshing(false), 500); } }, [queryClient]); if (!currentTask) { return ( <div className="flex items-center justify-center h-full"> <div className="text-center"> <p className="text-lg text-vscode-foreground/70 mb-4"> Task not found </p> <Button onClick={onNavigateBack} variant="outline"> Back to Kanban Board </Button> </div> </div> ); } return ( <div className="h-full flex flex-col"> <div className="flex-1 grid grid-cols-1 md:grid-cols-3 gap-6 p-6 overflow-auto"> {/* Left column - Main content (2/3 width) */} <div className="md:col-span-2 space-y-6"> {/* Breadcrumb navigation */} <div className="flex items-center justify-between"> <Breadcrumb> <BreadcrumbList> <BreadcrumbItem> <BreadcrumbLink onClick={onNavigateBack} className="cursor-pointer hover:text-vscode-foreground text-link" > Kanban Board </BreadcrumbLink> </BreadcrumbItem> {isSubtask && parentTask && ( <> <BreadcrumbSeparator /> <BreadcrumbItem> <BreadcrumbLink onClick={() => onNavigateToTask(parentTask.id)} className="cursor-pointer hover:text-vscode-foreground" > {parentTask.title} </BreadcrumbLink> </BreadcrumbItem> </> )} <BreadcrumbSeparator /> <BreadcrumbItem> <span className="text-vscode-foreground"> #{displayId} {currentTask.title} </span> </BreadcrumbItem> </BreadcrumbList> </Breadcrumb> <button onClick={handleRefresh} disabled={isRefreshing} className="p-1.5 rounded hover:bg-vscode-button-hoverBackground transition-colors" title="Refresh task details" > <RefreshCw className={`w-4 h-4 text-vscode-foreground/70 ${isRefreshing ? 'animate-spin' : ''}`} /> </button> </div> {/* Task ID and title */} <h1 className="text-2xl font-bold tracking-tight text-vscode-foreground"> #{displayId} {currentTask.title} </h1> {/* Description */} <div className="mb-8"> <p className="text-vscode-foreground/80 leading-relaxed"> {currentTask.description || 'No description available.'} </p> </div> {/* AI Actions */} <AIActionsSection currentTask={currentTask} isSubtask={isSubtask} parentTask={parentTask} sendMessage={sendMessage} refreshComplexityAfterAI={refreshComplexityAfterAI} /> {/* Implementation Details */} <DetailsSection title="Implementation Details" content={taskFileData.details} error={taskFileDataError} emptyMessage="No implementation details available" defaultExpanded={false} /> {/* Test Strategy */} <DetailsSection title="Test Strategy" content={taskFileData.testStrategy} error={taskFileDataError} emptyMessage="No test strategy available" defaultExpanded={false} /> {/* Subtasks */} <SubtasksSection currentTask={currentTask} isSubtask={isSubtask} sendMessage={sendMessage} onNavigateToTask={onNavigateToTask} /> </div> {/* Right column - Metadata (1/3 width) */} <TaskMetadataSidebar currentTask={currentTask} tasks={allTasks} complexity={complexity} isSubtask={isSubtask} onStatusChange={handleStatusChange} onDependencyClick={handleDependencyClick} /> </div> </div> ); }; export default TaskDetailsView; ``` -------------------------------------------------------------------------------- /src/profiles/kilo.js: -------------------------------------------------------------------------------- ```javascript // Kilo Code conversion profile for rule-transformer import path from 'path'; import fs from 'fs'; import { isSilentMode, log } from '../../scripts/modules/utils.js'; import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js'; import { ROO_MODES } from '../constants/profiles.js'; // Utility function to apply kilo transformations to content function applyKiloTransformations(content) { const customReplacements = [ // Replace roo-specific terms with kilo equivalents { from: /\broo\b/gi, to: (match) => (match.charAt(0) === 'R' ? 'Kilo' : 'kilo') }, { from: /Roo/g, to: 'Kilo' }, { from: /ROO/g, to: 'KILO' }, { from: /roocode\.com/gi, to: 'kilocode.com' }, { from: /docs\.roocode\.com/gi, to: 'docs.kilocode.com' }, { from: /https?:\/\/roocode\.com/gi, to: 'https://kilocode.com' }, { from: /https?:\/\/docs\.roocode\.com/gi, to: 'https://docs.kilocode.com' }, { from: /\.roo\//g, to: '.kilo/' }, { from: /\.roomodes/g, to: '.kilocodemodes' }, // Handle file extensions and directory references { from: /roo-rules/g, to: 'kilo-rules' }, { from: /rules-roo/g, to: 'rules-kilo' } ]; let transformedContent = content; for (const replacement of customReplacements) { transformedContent = transformedContent.replace( replacement.from, replacement.to ); } return transformedContent; } // Utility function to copy files recursively function copyRecursiveSync(src, dest) { const exists = fs.existsSync(src); const stats = exists && fs.statSync(src); const isDirectory = exists && stats.isDirectory(); if (isDirectory) { if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); fs.readdirSync(src).forEach((childItemName) => { copyRecursiveSync( path.join(src, childItemName), path.join(dest, childItemName) ); }); } else { fs.copyFileSync(src, dest); } } // Lifecycle functions for Kilo profile function onAddRulesProfile(targetDir, assetsDir) { // Use the provided assets directory to find the roocode directory const sourceDir = path.join(assetsDir, 'roocode'); if (!fs.existsSync(sourceDir)) { log('error', `[Kilo] Source directory does not exist: ${sourceDir}`); return; } // Copy basic roocode structure first copyRecursiveSync(sourceDir, targetDir); log('debug', `[Kilo] Copied roocode directory to ${targetDir}`); // Transform .roomodes to .kilocodemodes const roomodesSrc = path.join(sourceDir, '.roomodes'); const kilocodemodesDest = path.join(targetDir, '.kilocodemodes'); if (fs.existsSync(roomodesSrc)) { try { const roomodesContent = fs.readFileSync(roomodesSrc, 'utf8'); const transformedContent = applyKiloTransformations(roomodesContent); fs.writeFileSync(kilocodemodesDest, transformedContent); log('debug', `[Kilo] Created .kilocodemodes at ${kilocodemodesDest}`); // Remove the original .roomodes file fs.unlinkSync(path.join(targetDir, '.roomodes')); } catch (err) { log('error', `[Kilo] Failed to transform .roomodes: ${err.message}`); } } // Transform .roo directory to .kilo and apply kilo transformations to mode-specific rules const rooModesDir = path.join(sourceDir, '.roo'); const kiloModesDir = path.join(targetDir, '.kilo'); // Remove the copied .roo directory and create .kilo if (fs.existsSync(path.join(targetDir, '.roo'))) { fs.rmSync(path.join(targetDir, '.roo'), { recursive: true, force: true }); } for (const mode of ROO_MODES) { const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`); const dest = path.join(kiloModesDir, `rules-${mode}`, `${mode}-rules`); if (fs.existsSync(src)) { try { const destDir = path.dirname(dest); if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true }); // Read, transform, and write the rule file const ruleContent = fs.readFileSync(src, 'utf8'); const transformedContent = applyKiloTransformations(ruleContent); fs.writeFileSync(dest, transformedContent); log('debug', `[Kilo] Transformed and copied ${mode}-rules to ${dest}`); } catch (err) { log( 'error', `[Kilo] Failed to transform ${src} to ${dest}: ${err.message}` ); } } } } function onRemoveRulesProfile(targetDir) { const kilocodemodespath = path.join(targetDir, '.kilocodemodes'); if (fs.existsSync(kilocodemodespath)) { try { fs.rmSync(kilocodemodespath, { force: true }); log('debug', `[Kilo] Removed .kilocodemodes from ${kilocodemodespath}`); } catch (err) { log('error', `[Kilo] Failed to remove .kilocodemodes: ${err.message}`); } } const kiloDir = path.join(targetDir, '.kilo'); if (fs.existsSync(kiloDir)) { fs.readdirSync(kiloDir).forEach((entry) => { if (entry.startsWith('rules-')) { const modeDir = path.join(kiloDir, entry); try { fs.rmSync(modeDir, { recursive: true, force: true }); log('debug', `[Kilo] Removed ${entry} directory from ${modeDir}`); } catch (err) { log('error', `[Kilo] Failed to remove ${modeDir}: ${err.message}`); } } }); if (fs.readdirSync(kiloDir).length === 0) { try { fs.rmSync(kiloDir, { recursive: true, force: true }); log('debug', `[Kilo] Removed empty .kilo directory from ${kiloDir}`); } catch (err) { log('error', `[Kilo] Failed to remove .kilo directory: ${err.message}`); } } } } function onPostConvertRulesProfile(targetDir, assetsDir) { onAddRulesProfile(targetDir, assetsDir); } // Create and export kilo profile using the base factory with roo rule reuse export const kiloProfile = createProfile({ name: 'kilo', displayName: 'Kilo Code', url: 'kilocode.com', docsUrl: 'docs.kilocode.com', profileDir: '.kilo', rulesDir: '.kilo/rules', toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE, fileMap: { // Map roo rule files to kilo equivalents 'rules/cursor_rules.mdc': 'kilo_rules.md', 'rules/dev_workflow.mdc': 'dev_workflow.md', 'rules/self_improve.mdc': 'self_improve.md', 'rules/taskmaster.mdc': 'taskmaster.md' }, onAdd: onAddRulesProfile, onRemove: onRemoveRulesProfile, onPostConvert: onPostConvertRulesProfile }); // Export lifecycle functions separately to avoid naming conflicts export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile }; ``` -------------------------------------------------------------------------------- /apps/docs/getting-started/quick-start/installation.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Installation sidebarTitle: "Installation" --- Now that you have Node.js and your first API Key, you are ready to begin installing Task Master in one of three ways. <Note>Cursor Users Can Use the One Click Install Below</Note> <Accordion title="Quick Install for Cursor 1.0+ (One-Click)"> <a href="cursor://anysphere.cursor-deeplink/mcp/install?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIi0tcGFja2FnZT10YXNrLW1hc3Rlci1haSIsInRhc2stbWFzdGVyLWFpIl0sImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUJFX0FQSV9LRVkiOiJZT1VSX0FaVVJFX0tFWV9IRVJFIiwiT0xMQU1BX0FQSV9LRVkiOiJZT1VSX09MTEFNQV9BUElfS0VZX0hFUkUifX0%3D"> <img className="block dark:hidden" src="https://cursor.com/deeplink/mcp-install-light.png" alt="Add Task Master MCP server to Cursor" noZoom /> <img className="hidden dark:block" src="https://cursor.com/deeplink/mcp-install-dark.png" alt="Add Task Master MCP server to Cursor" noZoom /> </a> Or click the copy button (top-right of code block) then paste into your browser: ```text cursor://anysphere.cursor-deeplink/mcp/install?name=taskmaster-ai&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIi0tcGFja2FnZT10YXNrLW1hc3Rlci1haSIsInRhc2stbWFzdGVyLWFpIl0sImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQo= ``` > **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys. </Accordion> ## Installation Options <Accordion title="Option 1: MCP (Recommended)"> MCP (Model Control Protocol) lets you run Task Master directly from your editor. ## 1. Add your MCP config at the following path depending on your editor | Editor | Scope | Linux/macOS Path | Windows Path | Key | | ------------ | ------- | ------------------------------------- | ------------------------------------------------- | ------------ | | **Cursor** | Global | `~/.cursor/mcp.json` | `%USERPROFILE%\.cursor\mcp.json` | `mcpServers` | | | Project | `<project_folder>/.cursor/mcp.json` | `<project_folder>\.cursor\mcp.json` | `mcpServers` | | **Windsurf** | Global | `~/.codeium/windsurf/mcp_config.json` | `%USERPROFILE%\.codeium\windsurf\mcp_config.json` | `mcpServers` | | **VS Code** | Project | `<project_folder>/.vscode/mcp.json` | `<project_folder>\.vscode\mcp.json` | `servers` | ## Manual Configuration ### Cursor & Windsurf (`mcpServers`) ```json { "mcpServers": { "taskmaster-ai": { "command": "npx", "args": ["-y", "task-master-ai"], "env": { "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE", "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE", "GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE", "MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE", "OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE", "XAI_API_KEY": "YOUR_XAI_KEY_HERE", "AZURE_OPENAI_API_KEY": "YOUR_AZURE_KEY_HERE", "OLLAMA_API_KEY": "YOUR_OLLAMA_API_KEY_HERE" } } } } ``` > 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use. > **Note**: If you see `0 tools enabled` in the MCP settings, restart your editor and check that your API keys are correctly configured. ### VS Code (`servers` + `type`) ```json { "servers": { "taskmaster-ai": { "command": "npx", "args": ["-y", "task-master-ai"], "env": { "ANTHROPIC_API_KEY": "YOUR_ANTHROPIC_API_KEY_HERE", "PERPLEXITY_API_KEY": "YOUR_PERPLEXITY_API_KEY_HERE", "OPENAI_API_KEY": "YOUR_OPENAI_KEY_HERE", "GOOGLE_API_KEY": "YOUR_GOOGLE_KEY_HERE", "MISTRAL_API_KEY": "YOUR_MISTRAL_KEY_HERE", "OPENROUTER_API_KEY": "YOUR_OPENROUTER_KEY_HERE", "XAI_API_KEY": "YOUR_XAI_KEY_HERE", "AZURE_OPENAI_API_KEY": "YOUR_AZURE_KEY_HERE" }, "type": "stdio" } } } ``` > 🔑 Replace `YOUR_…_KEY_HERE` with your real API keys. You can remove keys you don't use. #### 2. (Cursor-only) Enable Taskmaster MCP Open Cursor Settings (Ctrl+Shift+J) ➡ Click on MCP tab on the left ➡ Enable task-master-ai with the toggle #### 3. (Optional) Configure the models you want to use In your editor's AI chat pane, say: ```txt Change the main, research and fallback models to <model_name>, <model_name> and <model_name> respectively. ``` For example, to use Claude Code (no API key required): ```txt Change the main model to claude-code/sonnet ``` #### 4. Initialize Task Master In your editor's AI chat pane, say: ```txt Initialize taskmaster-ai in my project ``` </Accordion> <Accordion title="Option 2: Using Command Line"> ## CLI Installation ```bash # Install globally npm install -g task-master-ai # OR install locally within your project npm install task-master-ai ``` ## Initialize a new project ```bash # If installed globally task-master init # If installed locally npx task-master init # Initialize project with specific rules task-master init --rules cursor,windsurf,vscode ``` This will prompt you for project details and set up a new project with the necessary files and structure. </Accordion> ``` -------------------------------------------------------------------------------- /tests/unit/profiles/rule-transformer-roo.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Mock fs module before importing anything that uses it jest.mock('fs', () => ({ readFileSync: jest.fn(), writeFileSync: jest.fn(), existsSync: jest.fn(), mkdirSync: jest.fn() })); // Import modules after mocking import fs from 'fs'; import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; import { rooProfile } from '../../../src/profiles/roo.js'; describe('Roo Rule Transformer', () => { // Set up spies on the mocked modules const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); const mockExistsSync = jest.spyOn(fs, 'existsSync'); const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); const mockConsoleError = jest .spyOn(console, 'error') .mockImplementation(() => {}); beforeEach(() => { jest.clearAllMocks(); // Setup default mocks mockReadFileSync.mockReturnValue(''); mockWriteFileSync.mockImplementation(() => {}); mockExistsSync.mockReturnValue(true); mockMkdirSync.mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); it('should correctly convert basic terms', () => { const testContent = `--- description: Test Cursor rule for basic terms globs: **/* alwaysApply: true --- This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. Also has references to .mdc files.`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', rooProfile ); // Verify the function succeeded expect(result).toBe(true); // Verify file operations were called correctly expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8'); expect(mockWriteFileSync).toHaveBeenCalledTimes(1); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations expect(transformedContent).toContain('Roo'); expect(transformedContent).toContain('roocode.com'); expect(transformedContent).toContain('.md'); expect(transformedContent).not.toContain('cursor.so'); expect(transformedContent).not.toContain('Cursor rule'); }); it('should correctly convert tool references', () => { const testContent = `--- description: Test Cursor rule for tool references globs: **/* alwaysApply: true --- - Use the search tool to find code - The edit_file tool lets you modify files - run_command executes terminal commands - use_mcp connects to external services`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', rooProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations (Roo uses different tool names) expect(transformedContent).toContain('search_files tool'); expect(transformedContent).toContain('apply_diff tool'); expect(transformedContent).toContain('execute_command'); expect(transformedContent).toContain('use_mcp_tool'); }); it('should correctly update file references', () => { const testContent = `--- description: Test Cursor rule for file references globs: **/* alwaysApply: true --- This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', rooProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations - no taskmaster subdirectory for Roo expect(transformedContent).toContain('(.roo/rules/dev_workflow.md)'); // File path transformation - no taskmaster subdirectory for Roo expect(transformedContent).toContain('(.roo/rules/taskmaster.md)'); // File path transformation - no taskmaster subdirectory for Roo expect(transformedContent).not.toContain('(mdc:.cursor/rules/'); }); it('should handle file read errors', () => { // Mock file read to throw an error mockReadFileSync.mockImplementation(() => { throw new Error('File not found'); }); // Call the actual function const result = convertRuleToProfileRule( 'nonexistent.mdc', 'target.md', rooProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify writeFileSync was not called expect(mockWriteFileSync).not.toHaveBeenCalled(); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: File not found' ); }); it('should handle file write errors', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock file write to throw an error mockWriteFileSync.mockImplementation(() => { throw new Error('Permission denied'); }); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', rooProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: Permission denied' ); }); it('should create target directory if it does not exist', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock directory doesn't exist initially mockExistsSync.mockReturnValue(false); // Call the actual function convertRuleToProfileRule( 'source.mdc', 'some/deep/path/target.md', rooProfile ); // Verify directory creation was called expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', { recursive: true }); }); }); ``` -------------------------------------------------------------------------------- /tests/unit/profiles/rule-transformer-trae.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Mock fs module before importing anything that uses it jest.mock('fs', () => ({ readFileSync: jest.fn(), writeFileSync: jest.fn(), existsSync: jest.fn(), mkdirSync: jest.fn() })); // Import modules after mocking import fs from 'fs'; import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; import { traeProfile } from '../../../src/profiles/trae.js'; describe('Trae Rule Transformer', () => { // Set up spies on the mocked modules const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); const mockExistsSync = jest.spyOn(fs, 'existsSync'); const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); const mockConsoleError = jest .spyOn(console, 'error') .mockImplementation(() => {}); beforeEach(() => { jest.clearAllMocks(); // Setup default mocks mockReadFileSync.mockReturnValue(''); mockWriteFileSync.mockImplementation(() => {}); mockExistsSync.mockReturnValue(true); mockMkdirSync.mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); it('should correctly convert basic terms', () => { const testContent = `--- description: Test Cursor rule for basic terms globs: **/* alwaysApply: true --- This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. Also has references to .mdc files.`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', traeProfile ); // Verify the function succeeded expect(result).toBe(true); // Verify file operations were called correctly expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8'); expect(mockWriteFileSync).toHaveBeenCalledTimes(1); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations expect(transformedContent).toContain('Trae'); expect(transformedContent).toContain('trae.ai'); expect(transformedContent).toContain('.md'); expect(transformedContent).not.toContain('cursor.so'); expect(transformedContent).not.toContain('Cursor rule'); }); it('should correctly convert tool references', () => { const testContent = `--- description: Test Cursor rule for tool references globs: **/* alwaysApply: true --- - Use the search tool to find code - The edit_file tool lets you modify files - run_command executes terminal commands - use_mcp connects to external services`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', traeProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations (Trae uses standard tool names, so no transformation) expect(transformedContent).toContain('search tool'); expect(transformedContent).toContain('edit_file tool'); expect(transformedContent).toContain('run_command'); expect(transformedContent).toContain('use_mcp'); }); it('should correctly update file references', () => { const testContent = `--- description: Test Cursor rule for file references globs: **/* alwaysApply: true --- This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', traeProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations - no taskmaster subdirectory for Trae expect(transformedContent).toContain('(.trae/rules/dev_workflow.md)'); // File path transformation - no taskmaster subdirectory for Trae expect(transformedContent).toContain('(.trae/rules/taskmaster.md)'); // File path transformation - no taskmaster subdirectory for Trae expect(transformedContent).not.toContain('(mdc:.cursor/rules/'); }); it('should handle file read errors', () => { // Mock file read to throw an error mockReadFileSync.mockImplementation(() => { throw new Error('File not found'); }); // Call the actual function const result = convertRuleToProfileRule( 'nonexistent.mdc', 'target.md', traeProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify writeFileSync was not called expect(mockWriteFileSync).not.toHaveBeenCalled(); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: File not found' ); }); it('should handle file write errors', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock file write to throw an error mockWriteFileSync.mockImplementation(() => { throw new Error('Permission denied'); }); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', traeProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: Permission denied' ); }); it('should create target directory if it does not exist', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock directory doesn't exist initially mockExistsSync.mockReturnValue(false); // Call the actual function convertRuleToProfileRule( 'source.mdc', 'some/deep/path/target.md', traeProfile ); // Verify directory creation was called expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', { recursive: true }); }); }); ``` -------------------------------------------------------------------------------- /tests/unit/profiles/rule-transformer-kilo.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Mock fs module before importing anything that uses it jest.mock('fs', () => ({ readFileSync: jest.fn(), writeFileSync: jest.fn(), existsSync: jest.fn(), mkdirSync: jest.fn() })); // Import modules after mocking import fs from 'fs'; import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; import { kiloProfile } from '../../../src/profiles/kilo.js'; describe('Kilo Rule Transformer', () => { // Set up spies on the mocked modules const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); const mockExistsSync = jest.spyOn(fs, 'existsSync'); const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); const mockConsoleError = jest .spyOn(console, 'error') .mockImplementation(() => {}); beforeEach(() => { jest.clearAllMocks(); // Setup default mocks mockReadFileSync.mockReturnValue(''); mockWriteFileSync.mockImplementation(() => {}); mockExistsSync.mockReturnValue(true); mockMkdirSync.mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); it('should correctly convert basic terms', () => { const testContent = `--- description: Test Cursor rule for basic terms globs: **/* alwaysApply: true --- This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. Also has references to .mdc files.`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', kiloProfile ); // Verify the function succeeded expect(result).toBe(true); // Verify file operations were called correctly expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8'); expect(mockWriteFileSync).toHaveBeenCalledTimes(1); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations expect(transformedContent).toContain('Kilo'); expect(transformedContent).toContain('kilocode.com'); expect(transformedContent).toContain('.md'); expect(transformedContent).not.toContain('cursor.so'); expect(transformedContent).not.toContain('Cursor rule'); }); it('should correctly convert tool references', () => { const testContent = `--- description: Test Cursor rule for tool references globs: **/* alwaysApply: true --- - Use the search tool to find code - The edit_file tool lets you modify files - run_command executes terminal commands - use_mcp connects to external services`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', kiloProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations (Kilo uses different tool names) expect(transformedContent).toContain('search_files tool'); expect(transformedContent).toContain('apply_diff tool'); expect(transformedContent).toContain('execute_command'); expect(transformedContent).toContain('use_mcp_tool'); }); it('should correctly update file references', () => { const testContent = `--- description: Test Cursor rule for file references globs: **/* alwaysApply: true --- This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', kiloProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations - no taskmaster subdirectory for Kilo expect(transformedContent).toContain('(.kilo/rules/dev_workflow.md)'); // File path transformation for dev_workflow - no taskmaster subdirectory for Kilo expect(transformedContent).toContain('(.kilo/rules/taskmaster.md)'); // File path transformation for taskmaster - no taskmaster subdirectory for Kilo expect(transformedContent).not.toContain('(mdc:.cursor/rules/'); }); it('should handle file read errors', () => { // Mock file read to throw an error mockReadFileSync.mockImplementation(() => { throw new Error('File not found'); }); // Call the actual function const result = convertRuleToProfileRule( 'nonexistent.mdc', 'target.md', kiloProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify writeFileSync was not called expect(mockWriteFileSync).not.toHaveBeenCalled(); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: File not found' ); }); it('should handle file write errors', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock file write to throw an error mockWriteFileSync.mockImplementation(() => { throw new Error('Permission denied'); }); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', kiloProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: Permission denied' ); }); it('should create target directory if it does not exist', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock directory doesn't exist initially mockExistsSync.mockReturnValue(false); // Call the actual function convertRuleToProfileRule( 'source.mdc', 'some/deep/path/target.md', kiloProfile ); // Verify directory creation was called expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', { recursive: true }); }); }); ``` -------------------------------------------------------------------------------- /tests/unit/profiles/rule-transformer-zed.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Mock fs module before importing anything that uses it jest.mock('fs', () => ({ readFileSync: jest.fn(), writeFileSync: jest.fn(), existsSync: jest.fn(), mkdirSync: jest.fn() })); // Import modules after mocking import fs from 'fs'; import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; import { zedProfile } from '../../../src/profiles/zed.js'; describe('Zed Rule Transformer', () => { // Set up spies on the mocked modules const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); const mockExistsSync = jest.spyOn(fs, 'existsSync'); const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); const mockConsoleError = jest .spyOn(console, 'error') .mockImplementation(() => {}); beforeEach(() => { jest.clearAllMocks(); // Setup default mocks mockReadFileSync.mockReturnValue(''); mockWriteFileSync.mockImplementation(() => {}); mockExistsSync.mockReturnValue(true); mockMkdirSync.mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); it('should correctly convert basic terms', () => { const testContent = `--- description: Test Cursor rule for basic terms globs: **/* alwaysApply: true --- This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. Also has references to .mdc files.`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Mock file system operations mockExistsSync.mockReturnValue(true); // Call the function const result = convertRuleToProfileRule( 'test-source.mdc', 'test-target.md', zedProfile ); // Verify the result expect(result).toBe(true); expect(mockWriteFileSync).toHaveBeenCalledTimes(1); // Get the transformed content const transformedContent = mockWriteFileSync.mock.calls[0][1]; // Verify Cursor -> Zed transformations expect(transformedContent).toContain('zed.dev'); expect(transformedContent).toContain('Zed'); expect(transformedContent).not.toContain('cursor.so'); expect(transformedContent).not.toContain('Cursor'); expect(transformedContent).toContain('.md'); expect(transformedContent).not.toContain('.mdc'); }); it('should handle URL transformations', () => { const testContent = `Visit https://cursor.so/docs for more information. Also check out cursor.so and www.cursor.so for updates.`; mockReadFileSync.mockReturnValue(testContent); mockExistsSync.mockReturnValue(true); const result = convertRuleToProfileRule( 'test-source.mdc', 'test-target.md', zedProfile ); expect(result).toBe(true); const transformedContent = mockWriteFileSync.mock.calls[0][1]; // Verify URL transformations expect(transformedContent).toContain('https://zed.dev'); expect(transformedContent).toContain('zed.dev'); expect(transformedContent).not.toContain('cursor.so'); }); it('should handle file extension transformations', () => { const testContent = `This rule references file.mdc and another.mdc file. Use the .mdc extension for all rule files.`; mockReadFileSync.mockReturnValue(testContent); mockExistsSync.mockReturnValue(true); const result = convertRuleToProfileRule( 'test-source.mdc', 'test-target.md', zedProfile ); expect(result).toBe(true); const transformedContent = mockWriteFileSync.mock.calls[0][1]; // Verify file extension transformations expect(transformedContent).toContain('file.md'); expect(transformedContent).toContain('another.md'); expect(transformedContent).toContain('.md extension'); expect(transformedContent).not.toContain('.mdc'); }); it('should handle case variations', () => { const testContent = `CURSOR, Cursor, cursor should all be transformed.`; mockReadFileSync.mockReturnValue(testContent); mockExistsSync.mockReturnValue(true); const result = convertRuleToProfileRule( 'test-source.mdc', 'test-target.md', zedProfile ); expect(result).toBe(true); const transformedContent = mockWriteFileSync.mock.calls[0][1]; // Verify case transformations // Due to regex order, the case-insensitive rule runs first: // CURSOR -> Zed (because it starts with 'C'), Cursor -> Zed, cursor -> zed expect(transformedContent).toContain('Zed'); expect(transformedContent).toContain('zed'); expect(transformedContent).not.toContain('CURSOR'); expect(transformedContent).not.toContain('Cursor'); expect(transformedContent).not.toContain('cursor'); }); it('should create target directory if it does not exist', () => { const testContent = 'Test content'; mockReadFileSync.mockReturnValue(testContent); mockExistsSync.mockReturnValue(false); const result = convertRuleToProfileRule( 'test-source.mdc', 'nested/path/test-target.md', zedProfile ); expect(result).toBe(true); expect(mockMkdirSync).toHaveBeenCalledWith('nested/path', { recursive: true }); }); it('should handle file system errors gracefully', () => { mockReadFileSync.mockImplementation(() => { throw new Error('File not found'); }); const result = convertRuleToProfileRule( 'test-source.mdc', 'test-target.md', zedProfile ); expect(result).toBe(false); expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: File not found' ); }); it('should handle write errors gracefully', () => { mockReadFileSync.mockReturnValue('Test content'); mockWriteFileSync.mockImplementation(() => { throw new Error('Write permission denied'); }); const result = convertRuleToProfileRule( 'test-source.mdc', 'test-target.md', zedProfile ); expect(result).toBe(false); expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: Write permission denied' ); }); it('should verify profile configuration', () => { expect(zedProfile.profileName).toBe('zed'); expect(zedProfile.displayName).toBe('Zed'); expect(zedProfile.profileDir).toBe('.zed'); expect(zedProfile.mcpConfig).toBe(true); expect(zedProfile.mcpConfigName).toBe('settings.json'); expect(zedProfile.mcpConfigPath).toBe('.zed/settings.json'); expect(zedProfile.includeDefaultRules).toBe(false); expect(zedProfile.fileMap).toEqual({ 'AGENTS.md': '.rules' }); }); }); ``` -------------------------------------------------------------------------------- /tests/unit/profiles/rule-transformer-windsurf.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Mock fs module before importing anything that uses it jest.mock('fs', () => ({ readFileSync: jest.fn(), writeFileSync: jest.fn(), existsSync: jest.fn(), mkdirSync: jest.fn() })); // Import modules after mocking import fs from 'fs'; import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; import { windsurfProfile } from '../../../src/profiles/windsurf.js'; describe('Windsurf Rule Transformer', () => { // Set up spies on the mocked modules const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); const mockExistsSync = jest.spyOn(fs, 'existsSync'); const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); const mockConsoleError = jest .spyOn(console, 'error') .mockImplementation(() => {}); beforeEach(() => { jest.clearAllMocks(); // Setup default mocks mockReadFileSync.mockReturnValue(''); mockWriteFileSync.mockImplementation(() => {}); mockExistsSync.mockReturnValue(true); mockMkdirSync.mockImplementation(() => {}); }); afterAll(() => { jest.restoreAllMocks(); }); it('should correctly convert basic terms', () => { const testContent = `--- description: Test Cursor rule for basic terms globs: **/* alwaysApply: true --- This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. Also has references to .mdc files.`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', windsurfProfile ); // Verify the function succeeded expect(result).toBe(true); // Verify file operations were called correctly expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8'); expect(mockWriteFileSync).toHaveBeenCalledTimes(1); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations expect(transformedContent).toContain('Windsurf'); expect(transformedContent).toContain('windsurf.com'); expect(transformedContent).toContain('.md'); expect(transformedContent).not.toContain('cursor.so'); expect(transformedContent).not.toContain('Cursor rule'); }); it('should correctly convert tool references', () => { const testContent = `--- description: Test Cursor rule for tool references globs: **/* alwaysApply: true --- - Use the search tool to find code - The edit_file tool lets you modify files - run_command executes terminal commands - use_mcp connects to external services`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', windsurfProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations (Windsurf uses standard tool names, so no transformation) expect(transformedContent).toContain('search tool'); expect(transformedContent).toContain('edit_file tool'); expect(transformedContent).toContain('run_command'); expect(transformedContent).toContain('use_mcp'); }); it('should correctly update file references', () => { const testContent = `--- description: Test Cursor rule for file references globs: **/* alwaysApply: true --- This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and [taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`; // Mock file read to return our test content mockReadFileSync.mockReturnValue(testContent); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', windsurfProfile ); // Verify the function succeeded expect(result).toBe(true); // Get the transformed content that was written const writeCall = mockWriteFileSync.mock.calls[0]; const transformedContent = writeCall[1]; // Verify transformations - no taskmaster subdirectory for Windsurf expect(transformedContent).toContain('(.windsurf/rules/dev_workflow.md)'); // File path transformation - no taskmaster subdirectory for Windsurf expect(transformedContent).toContain('(.windsurf/rules/taskmaster.md)'); // File path transformation - no taskmaster subdirectory for Windsurf expect(transformedContent).not.toContain('(mdc:.cursor/rules/'); }); it('should handle file read errors', () => { // Mock file read to throw an error mockReadFileSync.mockImplementation(() => { throw new Error('File not found'); }); // Call the actual function const result = convertRuleToProfileRule( 'nonexistent.mdc', 'target.md', windsurfProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify writeFileSync was not called expect(mockWriteFileSync).not.toHaveBeenCalled(); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: File not found' ); }); it('should handle file write errors', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock file write to throw an error mockWriteFileSync.mockImplementation(() => { throw new Error('Permission denied'); }); // Call the actual function const result = convertRuleToProfileRule( 'source.mdc', 'target.md', windsurfProfile ); // Verify the function failed gracefully expect(result).toBe(false); // Verify error was logged expect(mockConsoleError).toHaveBeenCalledWith( 'Error converting rule file: Permission denied' ); }); it('should create target directory if it does not exist', () => { const testContent = 'test content'; mockReadFileSync.mockReturnValue(testContent); // Mock directory doesn't exist initially mockExistsSync.mockReturnValue(false); // Call the actual function convertRuleToProfileRule( 'source.mdc', 'some/deep/path/target.md', windsurfProfile ); // Verify directory creation was called expect(mockMkdirSync).toHaveBeenCalledWith('some/deep/path', { recursive: true }); }); }); ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/move-task.js: -------------------------------------------------------------------------------- ```javascript /** * tools/move-task.js * Tool for moving tasks or subtasks to a new position */ import { z } from 'zod'; import { handleApiResult, createErrorResponse, withNormalizedProjectRoot } from './utils.js'; import { moveTaskDirect, moveTaskCrossTagDirect } from '../core/task-master-core.js'; import { findTasksPath } from '../core/utils/path-utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js'; /** * Register the moveTask tool with the MCP server * @param {Object} server - FastMCP server instance */ export function registerMoveTaskTool(server) { server.addTool({ name: 'move_task', description: 'Move a task or subtask to a new position', parameters: z.object({ from: z .string() .describe( 'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")' ), to: z .string() .optional() .describe( 'ID of the destination (e.g., "7" or "7.3"). Required for within-tag moves. For cross-tag moves, if omitted, task will be moved to the target tag maintaining its ID' ), file: z.string().optional().describe('Custom path to tasks.json file'), projectRoot: z .string() .describe( 'Root directory of the project (typically derived from session)' ), tag: z.string().optional().describe('Tag context to operate on'), fromTag: z.string().optional().describe('Source tag for cross-tag moves'), toTag: z.string().optional().describe('Target tag for cross-tag moves'), withDependencies: z .boolean() .optional() .describe('Move dependent tasks along with main task'), ignoreDependencies: z .boolean() .optional() .describe('Break cross-tag dependencies during move') }), execute: withNormalizedProjectRoot(async (args, { log, session }) => { try { // Check if this is a cross-tag move const isCrossTagMove = args.fromTag && args.toTag && args.fromTag !== args.toTag; if (isCrossTagMove) { // Cross-tag move logic if (!args.from) { return createErrorResponse( 'Source IDs are required for cross-tag moves', 'MISSING_SOURCE_IDS' ); } // Warn if 'to' parameter is provided for cross-tag moves if (args.to) { log.warn( 'The "to" parameter is not used for cross-tag moves and will be ignored. Tasks retain their original IDs in the target tag.' ); } // Find tasks.json path if not provided let tasksJsonPath = args.file; if (!tasksJsonPath) { tasksJsonPath = findTasksPath(args, log); } // Use cross-tag move function return handleApiResult( await moveTaskCrossTagDirect( { sourceIds: args.from, sourceTag: args.fromTag, targetTag: args.toTag, withDependencies: args.withDependencies || false, ignoreDependencies: args.ignoreDependencies || false, tasksJsonPath, projectRoot: args.projectRoot }, log, { session } ), log, 'Error moving tasks between tags', undefined, args.projectRoot ); } else { // Within-tag move logic (existing functionality) if (!args.to) { return createErrorResponse( 'Destination ID is required for within-tag moves', 'MISSING_DESTINATION_ID' ); } const resolvedTag = resolveTag({ projectRoot: args.projectRoot, tag: args.tag }); // Find tasks.json path if not provided let tasksJsonPath = args.file; if (!tasksJsonPath) { tasksJsonPath = findTasksPath(args, log); } // Parse comma-separated IDs const fromIds = args.from.split(',').map((id) => id.trim()); const toIds = args.to.split(',').map((id) => id.trim()); // Validate matching IDs count if (fromIds.length !== toIds.length) { if (fromIds.length > 1) { const results = []; const skipped = []; // Move tasks one by one, only generate files on the last move for (let i = 0; i < fromIds.length; i++) { const fromId = fromIds[i]; const toId = toIds[i]; // Skip if source and destination are the same if (fromId === toId) { log.info(`Skipping ${fromId} -> ${toId} (same ID)`); skipped.push({ fromId, toId, reason: 'same ID' }); continue; } const shouldGenerateFiles = i === fromIds.length - 1; const result = await moveTaskDirect( { sourceId: fromId, destinationId: toId, tasksJsonPath, projectRoot: args.projectRoot, tag: resolvedTag, generateFiles: shouldGenerateFiles }, log, { session } ); if (!result.success) { log.error( `Failed to move ${fromId} to ${toId}: ${result.error.message}` ); } else { results.push(result.data); } } return handleApiResult( { success: true, data: { moves: results, skipped: skipped.length > 0 ? skipped : undefined, message: `Successfully moved ${results.length} tasks${skipped.length > 0 ? `, skipped ${skipped.length}` : ''}` } }, log, 'Error moving multiple tasks', undefined, args.projectRoot ); } return handleApiResult( { success: true, data: { moves: results, skippedMoves: skippedMoves, message: `Successfully moved ${results.length} tasks${skippedMoves.length > 0 ? `, skipped ${skippedMoves.length} moves` : ''}` } }, log, 'Error moving multiple tasks', undefined, args.projectRoot ); } else { // Moving a single task return handleApiResult( await moveTaskDirect( { sourceId: args.from, destinationId: args.to, tasksJsonPath, projectRoot: args.projectRoot, tag: resolvedTag, generateFiles: true }, log, { session } ), log, 'Error moving task', undefined, args.projectRoot ); } } } catch (error) { return createErrorResponse( `Failed to move task: ${error.message}`, 'MOVE_TASK_ERROR' ); } }) }); } ``` -------------------------------------------------------------------------------- /apps/extension/src/extension.ts: -------------------------------------------------------------------------------- ```typescript /** * TaskMaster Extension - Simplified Architecture * Only using patterns where they add real value */ import * as vscode from 'vscode'; import { ConfigService } from './services/config-service'; import { PollingService } from './services/polling-service'; import { createPollingStrategy } from './services/polling-strategies'; import { TaskRepository } from './services/task-repository'; import { TerminalManager } from './services/terminal-manager'; import { WebviewManager } from './services/webview-manager'; import { EventEmitter } from './utils/event-emitter'; import { ExtensionLogger } from './utils/logger'; import { MCPClientManager, createMCPConfigFromSettings } from './utils/mcpClient'; import { TaskMasterApi } from './utils/task-master-api'; import { SidebarWebviewManager } from './services/sidebar-webview-manager'; let logger: ExtensionLogger; let mcpClient: MCPClientManager; let api: TaskMasterApi; let repository: TaskRepository; let terminalManager: TerminalManager; let pollingService: PollingService; let webviewManager: WebviewManager; let events: EventEmitter; let configService: ConfigService; let sidebarManager: SidebarWebviewManager; export async function activate(context: vscode.ExtensionContext) { try { // Initialize logger (needed to prevent MCP stdio issues) logger = ExtensionLogger.getInstance(); logger.log('🎉 TaskMaster Extension activating...'); // Simple event emitter for webview communication events = new EventEmitter(); // Initialize MCP client mcpClient = new MCPClientManager(createMCPConfigFromSettings()); // Initialize API api = new TaskMasterApi(mcpClient); // Repository with caching (actually useful for performance) repository = new TaskRepository(api, logger); // Terminal manager for task execution terminalManager = new TerminalManager(context, logger); // Config service for TaskMaster config.json configService = new ConfigService(logger); // Polling service with strategy pattern (makes sense for different polling behaviors) const strategy = createPollingStrategy( vscode.workspace.getConfiguration('taskmaster') ); pollingService = new PollingService(repository, strategy, logger); // Webview manager (cleaner than global panel array) - create before connection webviewManager = new WebviewManager( context, repository, events, logger, terminalManager ); webviewManager.setConfigService(configService); // Sidebar webview manager sidebarManager = new SidebarWebviewManager(context.extensionUri); // Initialize connection await initializeConnection(); // Set MCP client and API after connection webviewManager.setMCPClient(mcpClient); webviewManager.setApi(api); sidebarManager.setApi(api); // Register commands registerCommands(context); // Handle polling lifecycle events.on('webview:opened', () => { if (webviewManager.getPanelCount() === 1) { pollingService.start(); } }); events.on('webview:closed', () => { if (webviewManager.getPanelCount() === 0) { pollingService.stop(); } }); // Forward repository updates to webviews repository.on('tasks:updated', (tasks) => { webviewManager.broadcast('tasksUpdated', { tasks, source: 'polling' }); }); logger.log('✅ TaskMaster Extension activated'); } catch (error) { logger?.error('Failed to activate', error); vscode.window.showErrorMessage( `Failed to activate TaskMaster: ${error instanceof Error ? error.message : 'Unknown error'}` ); } } async function initializeConnection() { try { logger.log('🔗 Connecting to TaskMaster...'); // Notify webviews that we're connecting if (webviewManager) { webviewManager.broadcast('connectionStatus', { isConnected: false, status: 'Connecting...' }); } await mcpClient.connect(); const testResult = await api.testConnection(); if (testResult.success) { logger.log('✅ Connected to TaskMaster'); vscode.window.showInformationMessage('TaskMaster connected!'); // Notify webviews that we're connected if (webviewManager) { webviewManager.broadcast('connectionStatus', { isConnected: true, status: 'Connected' }); } if (sidebarManager) { sidebarManager.updateConnectionStatus(); } } else { throw new Error(testResult.error || 'Connection test failed'); } } catch (error) { logger.error('Connection failed', error); // Notify webviews that connection failed if (webviewManager) { webviewManager.broadcast('connectionStatus', { isConnected: false, status: 'Disconnected' }); } if (sidebarManager) { sidebarManager.updateConnectionStatus(); } handleConnectionError(error); } } function handleConnectionError(error: any) { const message = error instanceof Error ? error.message : 'Unknown error'; if (message.includes('ENOENT') && message.includes('npx')) { vscode.window .showWarningMessage( 'TaskMaster: npx not found. Please ensure Node.js is installed.', 'Open Settings' ) .then((action) => { if (action === 'Open Settings') { vscode.commands.executeCommand( 'workbench.action.openSettings', '@ext:Hamster.task-master-hamster taskmaster' ); } }); } else { vscode.window.showWarningMessage( `TaskMaster connection failed: ${message}` ); } } function registerCommands(context: vscode.ExtensionContext) { // Main command context.subscriptions.push( vscode.commands.registerCommand('tm.showKanbanBoard', async () => { await webviewManager.createOrShowPanel(); }) ); // Utility commands context.subscriptions.push( vscode.commands.registerCommand('tm.refreshTasks', async () => { await repository.refresh(); vscode.window.showInformationMessage('Tasks refreshed!'); }) ); context.subscriptions.push( vscode.commands.registerCommand('tm.openSettings', () => { vscode.commands.executeCommand( 'workbench.action.openSettings', '@ext:Hamster.task-master-hamster taskmaster' ); }) ); // Register sidebar view provider context.subscriptions.push( vscode.window.registerWebviewViewProvider( 'taskmaster.welcome', sidebarManager ) ); } export async function deactivate() { logger?.log('👋 TaskMaster Extension deactivating...'); pollingService?.stop(); webviewManager?.dispose(); await terminalManager?.dispose(); api?.destroy(); mcpClient?.disconnect(); } ``` -------------------------------------------------------------------------------- /apps/extension/package.publish.json: -------------------------------------------------------------------------------- ```json { "name": "task-master-hamster", "displayName": "Taskmaster AI", "description": "A visual Kanban board interface for Taskmaster projects in VS Code", "version": "0.23.1", "publisher": "Hamster", "icon": "assets/icon.png", "engines": { "vscode": "^1.93.0" }, "categories": ["AI", "Visualization", "Education", "Other"], "keywords": [ "kanban", "kanban board", "productivity", "todo", "task tracking", "project management", "task-master", "task management", "agile", "scrum", "ai", "mcp", "model context protocol", "dashboard", "chatgpt", "claude", "openai", "anthropic", "task", "npm", "intellicode", "react", "typescript", "php", "python", "node", "planner", "organizer", "workflow", "boards", "cards" ], "repository": "https://github.com/eyaltoledano/claude-task-master", "activationEvents": ["onStartupFinished", "workspaceContains:.taskmaster/**"], "main": "./dist/extension.js", "contributes": { "viewsContainers": { "activitybar": [ { "id": "taskmaster", "title": "Taskmaster", "icon": "assets/sidebar-icon.svg" } ] }, "views": { "taskmaster": [ { "id": "taskmaster.welcome", "name": "Taskmaster", "type": "webview" } ] }, "commands": [ { "command": "tm.showKanbanBoard", "title": "Taskmaster: Show Board" }, { "command": "tm.checkConnection", "title": "Taskmaster: Check Connection" }, { "command": "tm.reconnect", "title": "Taskmaster: Reconnect" }, { "command": "tm.openSettings", "title": "Taskmaster: Open Settings" } ], "configuration": { "title": "Taskmaster Kanban", "properties": { "taskmaster.mcp.command": { "type": "string", "default": "npx", "description": "The command or absolute path to execute for the MCP server (e.g., 'npx' or '/usr/local/bin/task-master-ai')." }, "taskmaster.mcp.args": { "type": "array", "items": { "type": "string" }, "default": ["-y", "task-master-ai"], "description": "An array of arguments to pass to the MCP server command." }, "taskmaster.mcp.cwd": { "type": "string", "description": "Working directory for the Task Master MCP server (defaults to workspace root)" }, "taskmaster.mcp.env": { "type": "object", "description": "Environment variables for the Task Master MCP server" }, "taskmaster.mcp.timeout": { "type": "number", "default": 30000, "minimum": 1000, "maximum": 300000, "description": "Connection timeout in milliseconds" }, "taskmaster.mcp.maxReconnectAttempts": { "type": "number", "default": 5, "minimum": 1, "maximum": 20, "description": "Maximum number of reconnection attempts" }, "taskmaster.mcp.reconnectBackoffMs": { "type": "number", "default": 1000, "minimum": 100, "maximum": 10000, "description": "Initial reconnection backoff delay in milliseconds" }, "taskmaster.mcp.maxBackoffMs": { "type": "number", "default": 30000, "minimum": 1000, "maximum": 300000, "description": "Maximum reconnection backoff delay in milliseconds" }, "taskmaster.mcp.healthCheckIntervalMs": { "type": "number", "default": 15000, "minimum": 5000, "maximum": 60000, "description": "Health check interval in milliseconds" }, "taskmaster.mcp.requestTimeoutMs": { "type": "number", "default": 300000, "minimum": 30000, "maximum": 600000, "description": "MCP request timeout in milliseconds (default: 5 minutes)" }, "taskmaster.ui.autoRefresh": { "type": "boolean", "default": true, "description": "Automatically refresh tasks from the server" }, "taskmaster.ui.refreshIntervalMs": { "type": "number", "default": 10000, "minimum": 1000, "maximum": 300000, "description": "Auto-refresh interval in milliseconds" }, "taskmaster.ui.theme": { "type": "string", "enum": ["auto", "light", "dark"], "default": "auto", "description": "UI theme preference" }, "taskmaster.ui.showCompletedTasks": { "type": "boolean", "default": true, "description": "Show completed tasks in the Kanban board" }, "taskmaster.ui.taskDisplayLimit": { "type": "number", "default": 100, "minimum": 1, "maximum": 1000, "description": "Maximum number of tasks to display" }, "taskmaster.ui.showPriority": { "type": "boolean", "default": true, "description": "Show task priority indicators" }, "taskmaster.ui.showTaskIds": { "type": "boolean", "default": true, "description": "Show task IDs in the interface" }, "taskmaster.performance.maxConcurrentRequests": { "type": "number", "default": 5, "minimum": 1, "maximum": 20, "description": "Maximum number of concurrent MCP requests" }, "taskmaster.performance.requestTimeoutMs": { "type": "number", "default": 30000, "minimum": 1000, "maximum": 300000, "description": "Request timeout in milliseconds" }, "taskmaster.performance.cacheTasksMs": { "type": "number", "default": 5000, "minimum": 0, "maximum": 60000, "description": "Task cache duration in milliseconds" }, "taskmaster.performance.lazyLoadThreshold": { "type": "number", "default": 50, "minimum": 10, "maximum": 500, "description": "Number of tasks before enabling lazy loading" }, "taskmaster.debug.enableLogging": { "type": "boolean", "default": true, "description": "Enable debug logging" }, "taskmaster.debug.logLevel": { "type": "string", "enum": ["error", "warn", "info", "debug"], "default": "info", "description": "Logging level" }, "taskmaster.debug.enableConnectionMetrics": { "type": "boolean", "default": true, "description": "Enable connection performance metrics" }, "taskmaster.debug.saveEventLogs": { "type": "boolean", "default": false, "description": "Save event logs to files" }, "taskmaster.debug.maxEventLogSize": { "type": "number", "default": 1000, "minimum": 10, "maximum": 10000, "description": "Maximum number of events to keep in memory" } } } } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/rules.js: -------------------------------------------------------------------------------- ```javascript /** * rules.js * Direct function implementation for adding or removing rules */ import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; import { convertAllRulesToProfileRules, removeProfileRules, getRulesProfile, isValidProfile } from '../../../../src/utils/rule-transformer.js'; import { RULE_PROFILES } from '../../../../src/constants/profiles.js'; import { RULES_ACTIONS } from '../../../../src/constants/rules-actions.js'; import { wouldRemovalLeaveNoProfiles, getInstalledProfiles } from '../../../../src/utils/profiles.js'; import path from 'path'; import fs from 'fs'; /** * Direct function wrapper for adding or removing rules. * @param {Object} args - Command arguments * @param {"add"|"remove"} args.action - Action to perform: add or remove rules * @param {string[]} args.profiles - List of profiles to add or remove * @param {string} args.projectRoot - Absolute path to the project root * @param {boolean} [args.yes=true] - Run non-interactively * @param {Object} log - Logger object * @param {Object} context - Additional context (session) * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } */ export async function rulesDirect(args, log, context = {}) { enableSilentMode(); try { const { action, profiles, projectRoot, yes, force } = args; if ( !action || !Array.isArray(profiles) || profiles.length === 0 || !projectRoot ) { return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'action, profiles, and projectRoot are required.' } }; } const removalResults = []; const addResults = []; if (action === RULES_ACTIONS.REMOVE) { // Safety check: Ensure this won't remove all rule profiles (unless forced) if (!force && wouldRemovalLeaveNoProfiles(projectRoot, profiles)) { const installedProfiles = getInstalledProfiles(projectRoot); const remainingProfiles = installedProfiles.filter( (profile) => !profiles.includes(profile) ); return { success: false, error: { code: 'CRITICAL_REMOVAL_BLOCKED', message: `CRITICAL: This operation would remove ALL remaining rule profiles (${profiles.join(', ')}), leaving your project with no rules configurations. This could significantly impact functionality. Currently installed profiles: ${installedProfiles.join(', ')}. If you're certain you want to proceed, set force: true or use the CLI with --force flag.` } }; } for (const profile of profiles) { if (!isValidProfile(profile)) { removalResults.push({ profileName: profile, success: false, error: `The requested rule profile for '${profile}' is unavailable. Supported profiles are: ${RULE_PROFILES.join(', ')}.` }); continue; } const profileConfig = getRulesProfile(profile); const result = removeProfileRules(projectRoot, profileConfig); removalResults.push(result); } const successes = removalResults .filter((r) => r.success) .map((r) => r.profileName); const skipped = removalResults .filter((r) => r.skipped) .map((r) => r.profileName); const errors = removalResults.filter( (r) => r.error && !r.success && !r.skipped ); const withNotices = removalResults.filter((r) => r.notice); let summary = ''; if (successes.length > 0) { summary += `Successfully removed Task Master rules: ${successes.join(', ')}.`; } if (skipped.length > 0) { summary += `Skipped (default or protected): ${skipped.join(', ')}.`; } if (errors.length > 0) { summary += errors .map((r) => `Error removing ${r.profileName}: ${r.error}`) .join(' '); } if (withNotices.length > 0) { summary += ` Notices: ${withNotices.map((r) => `${r.profileName} - ${r.notice}`).join('; ')}.`; } disableSilentMode(); return { success: errors.length === 0, data: { summary, results: removalResults } }; } else if (action === RULES_ACTIONS.ADD) { for (const profile of profiles) { if (!isValidProfile(profile)) { addResults.push({ profileName: profile, success: false, error: `Profile not found: static import missing for '${profile}'. Valid profiles: ${RULE_PROFILES.join(', ')}` }); continue; } const profileConfig = getRulesProfile(profile); const { success, failed } = convertAllRulesToProfileRules( projectRoot, profileConfig ); // Determine paths const rulesDir = profileConfig.rulesDir; const profileRulesDir = path.join(projectRoot, rulesDir); const profileDir = profileConfig.profileDir; const mcpConfig = profileConfig.mcpConfig !== false; const mcpPath = mcpConfig && profileConfig.mcpConfigPath ? path.join(projectRoot, profileConfig.mcpConfigPath) : null; // Check what was created const mcpConfigCreated = mcpConfig && mcpPath ? fs.existsSync(mcpPath) : undefined; const rulesDirCreated = fs.existsSync(profileRulesDir); const profileFolderCreated = fs.existsSync( path.join(projectRoot, profileDir) ); const error = failed > 0 ? `${failed} rule files failed to convert.` : null; const resultObj = { profileName: profile, mcpConfigCreated, rulesDirCreated, profileFolderCreated, skipped: false, error, success: (mcpConfig ? mcpConfigCreated : true) && rulesDirCreated && success > 0 && !error }; addResults.push(resultObj); } const successes = addResults .filter((r) => r.success) .map((r) => r.profileName); const errors = addResults.filter((r) => r.error && !r.success); let summary = ''; if (successes.length > 0) { summary += `Successfully added rules: ${successes.join(', ')}.`; } if (errors.length > 0) { summary += errors .map((r) => ` Error adding ${r.profileName}: ${r.error}`) .join(' '); } disableSilentMode(); return { success: errors.length === 0, data: { summary, results: addResults } }; } else { disableSilentMode(); return { success: false, error: { code: 'INVALID_ACTION', message: `Unknown action. Use "${RULES_ACTIONS.ADD}" or "${RULES_ACTIONS.REMOVE}".` } }; } } catch (error) { disableSilentMode(); log.error(`[rulesDirect] Error: ${error.message}`); return { success: false, error: { code: error.code || 'RULES_ERROR', message: error.message } }; } } ```