This is page 7 of 38. Use http://codebase.md/eyaltoledano/claude-task-master?page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/remove-subtask.js: -------------------------------------------------------------------------------- ```javascript /** * Direct function wrapper for removeSubtask */ import { removeSubtask } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; /** * Remove a subtask from its parent task * @param {Object} args - Function arguments * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. * @param {string} args.id - Subtask ID in format "parentId.subtaskId" (required) * @param {boolean} [args.convert] - Whether to convert the subtask to a standalone task * @param {boolean} [args.skipGenerate] - Skip regenerating task files * @param {string} args.projectRoot - Project root path (for MCP/env fallback) * @param {string} args.tag - Tag for the task (optional) * @param {Object} log - Logger object * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>} */ export async function removeSubtaskDirect(args, log) { // Destructure expected args const { tasksJsonPath, id, convert, skipGenerate, projectRoot, tag } = args; try { // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); log.info(`Removing subtask with args: ${JSON.stringify(args)}`); // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('removeSubtaskDirect called without tasksJsonPath'); disableSilentMode(); // Disable before returning return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } if (!id) { disableSilentMode(); // Disable before returning return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: 'Subtask ID is required and must be in format "parentId.subtaskId"' } }; } // Validate subtask ID format if (!id.includes('.')) { disableSilentMode(); // Disable before returning return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: `Invalid subtask ID format: ${id}. Expected format: "parentId.subtaskId"` } }; } // Use provided path const tasksPath = tasksJsonPath; // Convert convertToTask to a boolean const convertToTask = convert === true; // Determine if we should generate files const generateFiles = !skipGenerate; log.info( `Removing subtask ${id} (convertToTask: ${convertToTask}, generateFiles: ${generateFiles})` ); // Use the provided tasksPath const result = await removeSubtask( tasksPath, id, convertToTask, generateFiles, { projectRoot, tag } ); // Restore normal logging disableSilentMode(); if (convertToTask && result) { // Return info about the converted task return { success: true, data: { message: `Subtask ${id} successfully converted to task #${result.id}`, task: result } }; } else { // Return simple success message for deletion return { success: true, data: { message: `Subtask ${id} successfully removed` } }; } } catch (error) { // Ensure silent mode is disabled even if an outer error occurs disableSilentMode(); log.error(`Error in removeSubtaskDirect: ${error.message}`); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /src/ui/confirm.js: -------------------------------------------------------------------------------- ```javascript import chalk from 'chalk'; import boxen from 'boxen'; /** * Confirm removing profile rules (destructive operation) * @param {string[]} profiles - Array of profile names to remove * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise */ async function confirmProfilesRemove(profiles) { const profileList = profiles .map((b) => b.charAt(0).toUpperCase() + b.slice(1)) .join(', '); console.log( boxen( chalk.yellow( `WARNING: This will selectively remove Task Master components for: ${profileList}. What will be removed: • Task Master specific rule files (e.g., cursor_rules.mdc, taskmaster.mdc, etc.) • Task Master MCP server configuration (if no other MCP servers exist) What will be preserved: • Your existing custom rule files • Other MCP server configurations • The profile directory itself (unless completely empty after removal) The .[profile] directory will only be removed if ALL of the following are true: • All rules in the directory were Task Master rules (no custom rules) • No other files or folders exist in the profile directory • The MCP configuration was completely removed (no other servers) Are you sure you want to proceed?` ), { padding: 1, borderColor: 'yellow', borderStyle: 'round' } ) ); const inquirer = await import('inquirer'); const { confirm } = await inquirer.default.prompt([ { type: 'confirm', name: 'confirm', message: 'Type y to confirm selective removal, or n to abort:', default: false } ]); return confirm; } /** * Confirm removing ALL remaining profile rules (extremely critical operation) * @param {string[]} profiles - Array of profile names to remove * @param {string[]} remainingProfiles - Array of profiles that would be left after removal * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise */ async function confirmRemoveAllRemainingProfiles(profiles, remainingProfiles) { const profileList = profiles .map((p) => p.charAt(0).toUpperCase() + p.slice(1)) .join(', '); console.log( boxen( chalk.red.bold( `⚠️ CRITICAL WARNING: REMOVING ALL TASK MASTER RULE PROFILES ⚠️\n\n` + `You are about to remove Task Master components for: ${profileList}\n` + `This will leave your project with NO Task Master rule profiles remaining!\n\n` + `What will be removed:\n` + `• All Task Master specific rule files\n` + `• Task Master MCP server configurations\n` + `• Profile directories (only if completely empty after removal)\n\n` + `What will be preserved:\n` + `• Your existing custom rule files\n` + `• Other MCP server configurations\n` + `• Profile directories with custom content\n\n` + `This could impact Task Master functionality but will preserve your custom configurations.\n\n` + `Are you absolutely sure you want to proceed?` ), { padding: 1, borderColor: 'red', borderStyle: 'double', title: '🚨 CRITICAL OPERATION', titleAlignment: 'center' } ) ); const inquirer = await import('inquirer'); const { confirm } = await inquirer.default.prompt([ { type: 'confirm', name: 'confirm', message: 'Type y to confirm removing ALL Task Master rule profiles, or n to abort:', default: false } ]); return confirm; } export { confirmProfilesRemove, confirmRemoveAllRemainingProfiles }; ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/update-single-task-status.js: -------------------------------------------------------------------------------- ```javascript import chalk from 'chalk'; import { log } from '../utils.js'; import { isValidTaskStatus } from '../../../src/constants/task-status.js'; /** * Update the status of a single task * @param {string} tasksPath - Path to the tasks.json file * @param {string} taskIdInput - Task ID to update * @param {string} newStatus - New status * @param {Object} data - Tasks data * @param {boolean} showUi - Whether to show UI elements */ async function updateSingleTaskStatus( tasksPath, taskIdInput, newStatus, data, showUi = true ) { if (!isValidTaskStatus(newStatus)) { throw new Error( `Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}` ); } // Check if it's a subtask (e.g., "1.2") if (taskIdInput.includes('.')) { const [parentId, subtaskId] = taskIdInput .split('.') .map((id) => parseInt(id, 10)); // Find the parent task const parentTask = data.tasks.find((t) => t.id === parentId); if (!parentTask) { throw new Error(`Parent task ${parentId} not found`); } // Find the subtask if (!parentTask.subtasks) { throw new Error(`Parent task ${parentId} has no subtasks`); } const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); if (!subtask) { throw new Error( `Subtask ${subtaskId} not found in parent task ${parentId}` ); } // Update the subtask status const oldStatus = subtask.status || 'pending'; subtask.status = newStatus; log( 'info', `Updated subtask ${parentId}.${subtaskId} status from '${oldStatus}' to '${newStatus}'` ); // Check if all subtasks are done (if setting to 'done') if ( newStatus.toLowerCase() === 'done' || newStatus.toLowerCase() === 'completed' ) { const allSubtasksDone = parentTask.subtasks.every( (st) => st.status === 'done' || st.status === 'completed' ); // Suggest updating parent task if all subtasks are done if ( allSubtasksDone && parentTask.status !== 'done' && parentTask.status !== 'completed' ) { // Only show suggestion in CLI mode if (showUi) { console.log( chalk.yellow( `All subtasks of parent task ${parentId} are now marked as done.` ) ); console.log( chalk.yellow( `Consider updating the parent task status with: task-master set-status --id=${parentId} --status=done` ) ); } } } } else { // Handle regular task const taskId = parseInt(taskIdInput, 10); const task = data.tasks.find((t) => t.id === taskId); if (!task) { throw new Error(`Task ${taskId} not found`); } // Update the task status const oldStatus = task.status || 'pending'; task.status = newStatus; log( 'info', `Updated task ${taskId} status from '${oldStatus}' to '${newStatus}'` ); // If marking as done, also mark all subtasks as done if ( (newStatus.toLowerCase() === 'done' || newStatus.toLowerCase() === 'completed') && task.subtasks && task.subtasks.length > 0 ) { const pendingSubtasks = task.subtasks.filter( (st) => st.status !== 'done' && st.status !== 'completed' ); if (pendingSubtasks.length > 0) { log( 'info', `Also marking ${pendingSubtasks.length} subtasks as '${newStatus}'` ); pendingSubtasks.forEach((subtask) => { subtask.status = newStatus; }); } } } } export default updateSingleTaskStatus; ``` -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- ```yaml name: CI on: push: branches: - main - next pull_request: branches: - main - next workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true permissions: contents: read env: DO_NOT_TRACK: 1 NODE_ENV: development jobs: # Fast checks that can run in parallel format-check: name: Format Check runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - name: Install dependencies run: npm install --frozen-lockfile --prefer-offline timeout-minutes: 5 - name: Format Check run: npm run format-check env: FORCE_COLOR: 1 typecheck: name: Typecheck timeout-minutes: 10 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - name: Install dependencies run: npm install --frozen-lockfile --prefer-offline timeout-minutes: 5 - name: Typecheck run: npm run turbo:typecheck env: FORCE_COLOR: 1 # Build job to ensure everything compiles build: name: Build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - name: Install dependencies run: npm install --frozen-lockfile --prefer-offline timeout-minutes: 5 - name: Build run: npm run turbo:build env: NODE_ENV: production FORCE_COLOR: 1 TM_PUBLIC_BASE_DOMAIN: ${{ secrets.TM_PUBLIC_BASE_DOMAIN }} TM_PUBLIC_SUPABASE_URL: ${{ secrets.TM_PUBLIC_SUPABASE_URL }} TM_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.TM_PUBLIC_SUPABASE_ANON_KEY }} - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-artifacts path: dist/ retention-days: 1 test: name: Test timeout-minutes: 15 runs-on: ubuntu-latest needs: [format-check, typecheck, build] steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - uses: actions/setup-node@v4 with: node-version: 20 cache: "npm" - name: Install dependencies run: npm install --frozen-lockfile --prefer-offline timeout-minutes: 5 - name: Download build artifacts uses: actions/download-artifact@v4 with: name: build-artifacts path: dist/ - name: Run Tests run: | npm run test:coverage -- --coverageThreshold '{"global":{"branches":0,"functions":0,"lines":0,"statements":0}}' --detectOpenHandles --forceExit env: NODE_ENV: test CI: true FORCE_COLOR: 1 - name: Upload Test Results if: always() uses: actions/upload-artifact@v4 with: name: test-results path: | test-results coverage junit.xml retention-days: 30 ``` -------------------------------------------------------------------------------- /apps/extension/src/components/TaskDetails/useTaskDetails.ts: -------------------------------------------------------------------------------- ```typescript import { useMemo } from 'react'; import { useTaskDetails as useTaskDetailsQuery } from '../../webview/hooks/useTaskQueries'; import type { TaskMasterTask } from '../../webview/types'; interface TaskFileData { details?: string; testStrategy?: string; } interface UseTaskDetailsProps { taskId: string; sendMessage: (message: any) => Promise<any>; tasks: TaskMasterTask[]; } export const useTaskDetails = ({ taskId, sendMessage, tasks }: UseTaskDetailsProps) => { // Parse task ID to determine if it's a subtask (e.g., "13.2") const { isSubtask, parentId, subtaskIndex, taskIdForFetch } = useMemo(() => { // Ensure taskId is a string const taskIdStr = String(taskId); const parts = taskIdStr.split('.'); if (parts.length === 2) { return { isSubtask: true, parentId: parts[0], subtaskIndex: parseInt(parts[1]) - 1, // Convert to 0-based index taskIdForFetch: parts[0] // Always fetch parent task for subtasks }; } return { isSubtask: false, parentId: taskIdStr, subtaskIndex: -1, taskIdForFetch: taskIdStr }; }, [taskId]); // Use React Query to fetch full task details const { data: fullTaskData, error: taskDetailsError } = useTaskDetailsQuery(taskIdForFetch); // Find current task from local state for immediate display const { currentTask, parentTask } = useMemo(() => { if (isSubtask) { const parent = tasks.find((t) => t.id === parentId); if (parent && parent.subtasks && parent.subtasks[subtaskIndex]) { const subtask = parent.subtasks[subtaskIndex]; return { currentTask: subtask, parentTask: parent }; } } else { const task = tasks.find((t) => t.id === String(taskId)); if (task) { return { currentTask: task, parentTask: null }; } } return { currentTask: null, parentTask: null }; }, [taskId, tasks, isSubtask, parentId, subtaskIndex]); // Merge full task data from React Query with local state const mergedCurrentTask = useMemo(() => { if (!currentTask || !fullTaskData) return currentTask; if (isSubtask && fullTaskData.subtasks) { // Find the specific subtask in the full data const subtaskData = fullTaskData.subtasks.find( (st: any) => st.id === currentTask.id || st.id === parseInt(currentTask.id as any) ); if (subtaskData) { return { ...currentTask, ...subtaskData }; } } else if (!isSubtask) { // Merge parent task data return { ...currentTask, ...fullTaskData }; } return currentTask; }, [currentTask, fullTaskData, isSubtask]); // Extract task file data const taskFileData: TaskFileData = useMemo(() => { if (!mergedCurrentTask) return {}; return { details: mergedCurrentTask.details || '', testStrategy: mergedCurrentTask.testStrategy || '' }; }, [mergedCurrentTask]); // Get complexity score const complexity = useMemo(() => { if (mergedCurrentTask?.complexityScore !== undefined) { return { score: mergedCurrentTask.complexityScore }; } return null; }, [mergedCurrentTask]); // Function to refresh data after AI operations const refreshComplexityAfterAI = () => { // React Query will automatically refetch when mutations invalidate the query // No need for manual refresh }; return { currentTask: mergedCurrentTask, parentTask, isSubtask, taskFileData, taskFileDataError: taskDetailsError ? 'Failed to load task details' : null, complexity, refreshComplexityAfterAI }; }; ``` -------------------------------------------------------------------------------- /tests/unit/kebab-case-validation.test.js: -------------------------------------------------------------------------------- ```javascript /** * Kebab case validation tests */ import { jest } from '@jest/globals'; import { toKebabCase } from '../../scripts/modules/utils.js'; // Create a test implementation of detectCamelCaseFlags function testDetectCamelCaseFlags(args) { const camelCaseFlags = []; for (const arg of args) { if (arg.startsWith('--')) { const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after = // Skip single-word flags - they can't be camelCase if (!flagName.includes('-') && !/[A-Z]/.test(flagName)) { continue; } // Check for camelCase pattern (lowercase followed by uppercase) if (/[a-z][A-Z]/.test(flagName)) { const kebabVersion = toKebabCase(flagName); if (kebabVersion !== flagName) { camelCaseFlags.push({ original: flagName, kebabCase: kebabVersion }); } } } } return camelCaseFlags; } describe('Kebab Case Validation', () => { describe('toKebabCase', () => { test('should convert camelCase to kebab-case', () => { expect(toKebabCase('promptText')).toBe('prompt-text'); expect(toKebabCase('userID')).toBe('user-id'); expect(toKebabCase('numTasks')).toBe('num-tasks'); }); test('should handle already kebab-case strings', () => { expect(toKebabCase('already-kebab-case')).toBe('already-kebab-case'); expect(toKebabCase('kebab-case')).toBe('kebab-case'); }); test('should handle single words', () => { expect(toKebabCase('single')).toBe('single'); expect(toKebabCase('file')).toBe('file'); }); }); describe('detectCamelCaseFlags', () => { test('should properly detect camelCase flags', () => { const args = [ 'node', 'task-master', 'add-task', '--promptText=test', '--userID=123' ]; const flags = testDetectCamelCaseFlags(args); expect(flags).toHaveLength(2); expect(flags).toContainEqual({ original: 'promptText', kebabCase: 'prompt-text' }); expect(flags).toContainEqual({ original: 'userID', kebabCase: 'user-id' }); }); test('should not flag kebab-case or lowercase flags', () => { const args = [ 'node', 'task-master', 'add-task', '--prompt=test', '--user-id=123' ]; const flags = testDetectCamelCaseFlags(args); expect(flags).toHaveLength(0); }); test('should not flag any single-word flags regardless of case', () => { const args = [ 'node', 'task-master', 'add-task', '--prompt=test', // lowercase '--PROMPT=test', // uppercase '--Prompt=test', // mixed case '--file=test', // lowercase '--FILE=test', // uppercase '--File=test' // mixed case ]; const flags = testDetectCamelCaseFlags(args); expect(flags).toHaveLength(0); }); test('should handle mixed case flags correctly', () => { const args = [ 'node', 'task-master', 'add-task', '--prompt=test', // single word, should pass '--promptText=test', // camelCase, should flag '--prompt-text=test', // kebab-case, should pass '--ID=123', // single word, should pass '--userId=123', // camelCase, should flag '--user-id=123' // kebab-case, should pass ]; const flags = testDetectCamelCaseFlags(args); expect(flags).toHaveLength(2); expect(flags).toContainEqual({ original: 'promptText', kebabCase: 'prompt-text' }); expect(flags).toContainEqual({ original: 'userId', kebabCase: 'user-id' }); }); }); }); ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/find-next-task.test.js: -------------------------------------------------------------------------------- ```javascript /** * Tests for the find-next-task.js module */ import { jest } from '@jest/globals'; import findNextTask from '../../../../../scripts/modules/task-manager/find-next-task.js'; describe('findNextTask', () => { test('should return the highest priority task with all dependencies satisfied', () => { const tasks = [ { id: 1, title: 'Setup Project', status: 'done', dependencies: [], priority: 'high' }, { id: 2, title: 'Implement Core Features', status: 'pending', dependencies: [1], priority: 'high' }, { id: 3, title: 'Create Documentation', status: 'pending', dependencies: [1], priority: 'medium' }, { id: 4, title: 'Deploy Application', status: 'pending', dependencies: [2, 3], priority: 'high' } ]; const nextTask = findNextTask(tasks); expect(nextTask).toBeDefined(); expect(nextTask.id).toBe(2); expect(nextTask.title).toBe('Implement Core Features'); }); test('should prioritize by priority level when dependencies are equal', () => { const tasks = [ { id: 1, title: 'Setup Project', status: 'done', dependencies: [], priority: 'high' }, { id: 2, title: 'Low Priority Task', status: 'pending', dependencies: [1], priority: 'low' }, { id: 3, title: 'Medium Priority Task', status: 'pending', dependencies: [1], priority: 'medium' }, { id: 4, title: 'High Priority Task', status: 'pending', dependencies: [1], priority: 'high' } ]; const nextTask = findNextTask(tasks); expect(nextTask.id).toBe(4); expect(nextTask.priority).toBe('high'); }); test('should return null when all tasks are completed', () => { const tasks = [ { id: 1, title: 'Setup Project', status: 'done', dependencies: [], priority: 'high' }, { id: 2, title: 'Implement Features', status: 'done', dependencies: [1], priority: 'high' } ]; const nextTask = findNextTask(tasks); expect(nextTask).toBeNull(); }); test('should return null when all pending tasks have unsatisfied dependencies', () => { const tasks = [ { id: 1, title: 'Setup Project', status: 'pending', dependencies: [2], priority: 'high' }, { id: 2, title: 'Implement Features', status: 'pending', dependencies: [1], priority: 'high' } ]; const nextTask = findNextTask(tasks); expect(nextTask).toBeNull(); }); test('should handle empty tasks array', () => { const nextTask = findNextTask([]); expect(nextTask).toBeNull(); }); test('should consider subtask dependencies when finding next task', () => { const tasks = [ { id: 1, title: 'Parent Task', status: 'in-progress', dependencies: [], priority: 'high', subtasks: [ { id: 1, title: 'Subtask 1', status: 'done', dependencies: [] }, { id: 2, title: 'Subtask 2', status: 'pending', dependencies: [] } ] }, { id: 2, title: 'Dependent Task', status: 'pending', dependencies: [1], priority: 'high' } ]; const nextTask = findNextTask(tasks); // Task 2 should not be returned because Task 1 is not completely done // (it has a pending subtask) expect(nextTask).not.toEqual(expect.objectContaining({ id: 2 })); }); }); ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/scope-up.js: -------------------------------------------------------------------------------- ```javascript /** * scope-up.js * Direct function implementation for scoping up task complexity */ import { scopeUpTask } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; /** * Direct function wrapper for scoping up task complexity with error handling. * * @param {Object} args - Command arguments * @param {string} args.id - Comma-separated list of task IDs to scope up * @param {string} [args.strength='regular'] - Strength level (light, regular, heavy) * @param {string} [args.prompt] - Custom prompt for scoping adjustments * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) * @param {boolean} [args.research=false] - Whether to use research capabilities for scoping * @param {string} args.projectRoot - Project root path * @param {string} [args.tag] - Tag for the task context (optional) * @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 scopeUpDirect(args, log, context = {}) { // Destructure expected args const { tasksJsonPath, id, strength = 'regular', prompt: customPrompt, research = false, projectRoot, tag } = args; const { session } = context; // Destructure session from context // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); // Create logger wrapper using the utility const mcpLog = createLogWrapper(log); try { // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('scopeUpDirect called without tasksJsonPath'); disableSilentMode(); // Disable before returning return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Check required parameters if (!id) { log.error('Missing required parameter: id'); disableSilentMode(); return { success: false, error: { code: 'MISSING_PARAMETER', message: 'The id parameter is required for scoping up tasks' } }; } // Parse task IDs - convert to numbers as expected by scopeUpTask const taskIds = id.split(',').map((taskId) => parseInt(taskId.trim(), 10)); log.info( `Scoping up tasks: ${taskIds.join(', ')}, strength: ${strength}, research: ${research}` ); // Call the scopeUpTask function const result = await scopeUpTask( tasksJsonPath, taskIds, strength, customPrompt, { session, mcpLog, projectRoot, commandName: 'scope-up', outputType: 'mcp', tag, research }, 'json' // outputFormat ); // Restore normal logging disableSilentMode(); return { success: true, data: { updatedTasks: result.updatedTasks, tasksUpdated: result.updatedTasks.length, message: `Successfully scoped up ${result.updatedTasks.length} task(s)`, telemetryData: result.telemetryData } }; } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error in scopeUpDirect: ${error.message}`); return { success: false, error: { code: error.code || 'SCOPE_UP_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/claude-code/types.js: -------------------------------------------------------------------------------- ```javascript /** * @fileoverview Type definitions for Claude Code AI SDK provider * These JSDoc types mirror the TypeScript interfaces from the original provider */ /** * Claude Code provider settings * @typedef {Object} ClaudeCodeSettings * @property {string} [pathToClaudeCodeExecutable='claude'] - Custom path to Claude Code CLI executable * @property {string} [customSystemPrompt] - Custom system prompt to use * @property {string} [appendSystemPrompt] - Append additional content to the system prompt * @property {number} [maxTurns] - Maximum number of turns for the conversation * @property {number} [maxThinkingTokens] - Maximum thinking tokens for the model * @property {string} [cwd] - Working directory for CLI operations * @property {'bun'|'deno'|'node'} [executable='node'] - JavaScript runtime to use * @property {string[]} [executableArgs] - Additional arguments for the JavaScript runtime * @property {'default'|'acceptEdits'|'bypassPermissions'|'plan'} [permissionMode='default'] - Permission mode for tool usage * @property {string} [permissionPromptToolName] - Custom tool name for permission prompts * @property {boolean} [continue] - Continue the most recent conversation * @property {string} [resume] - Resume a specific session by ID * @property {string[]} [allowedTools] - Tools to explicitly allow during execution (e.g., ['Read', 'LS', 'Bash(git log:*)']) * @property {string[]} [disallowedTools] - Tools to disallow during execution (e.g., ['Write', 'Edit', 'Bash(rm:*)']) * @property {Object.<string, MCPServerConfig>} [mcpServers] - MCP server configuration * @property {boolean} [verbose] - Enable verbose logging for debugging */ /** * MCP Server configuration * @typedef {Object} MCPServerConfig * @property {'stdio'|'sse'} [type='stdio'] - Server type * @property {string} command - Command to execute (for stdio type) * @property {string[]} [args] - Arguments for the command * @property {Object.<string, string>} [env] - Environment variables * @property {string} url - URL for SSE type servers * @property {Object.<string, string>} [headers] - Headers for SSE type servers */ /** * Model ID type - either 'opus', 'sonnet', or any string * @typedef {'opus'|'sonnet'|string} ClaudeCodeModelId */ /** * Language model options * @typedef {Object} ClaudeCodeLanguageModelOptions * @property {ClaudeCodeModelId} id - The model ID * @property {ClaudeCodeSettings} [settings] - Optional settings */ /** * Error metadata for Claude Code errors * @typedef {Object} ClaudeCodeErrorMetadata * @property {string} [code] - Error code * @property {number} [exitCode] - Process exit code * @property {string} [stderr] - Standard error output * @property {string} [promptExcerpt] - Excerpt of the prompt that caused the error */ /** * Claude Code provider interface * @typedef {Object} ClaudeCodeProvider * @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} languageModel - Create a language model * @property {function(ClaudeCodeModelId, ClaudeCodeSettings=): Object} chat - Alias for languageModel * @property {function(string): never} textEmbeddingModel - Throws NoSuchModelError (not supported) */ /** * Claude Code provider settings * @typedef {Object} ClaudeCodeProviderSettings * @property {ClaudeCodeSettings} [defaultSettings] - Default settings to use for all models */ export {}; // This ensures the file is treated as a module ``` -------------------------------------------------------------------------------- /tests/unit/profiles/opencode-integration.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; import fs from 'fs'; import path from 'path'; import os from 'os'; describe('OpenCode Profile Integration', () => { let tempDir; beforeEach(() => { jest.clearAllMocks(); // Create a temporary directory for testing tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); // Spy on fs methods jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { if (filePath.toString().includes('AGENTS.md')) { return 'Sample AGENTS.md content for OpenCode integration'; } if (filePath.toString().includes('opencode.json')) { return JSON.stringify({ mcpServers: {} }, null, 2); } return '{}'; }); jest.spyOn(fs, 'existsSync').mockImplementation(() => false); jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); }); afterEach(() => { // Clean up the temporary directory try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (err) { console.error(`Error cleaning up: ${err.message}`); } }); // Test function that simulates the OpenCode profile file copying behavior function mockCreateOpenCodeStructure() { // OpenCode profile copies AGENTS.md to AGENTS.md in project root (same name) const sourceContent = 'Sample AGENTS.md content for OpenCode integration'; fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), sourceContent); // OpenCode profile creates opencode.json config file const configContent = JSON.stringify({ mcpServers: {} }, null, 2); fs.writeFileSync(path.join(tempDir, 'opencode.json'), configContent); } test('creates AGENTS.md file in project root', () => { // Act mockCreateOpenCodeStructure(); // Assert expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, 'AGENTS.md'), 'Sample AGENTS.md content for OpenCode integration' ); }); test('creates opencode.json config file in project root', () => { // Act mockCreateOpenCodeStructure(); // Assert expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, 'opencode.json'), JSON.stringify({ mcpServers: {} }, null, 2) ); }); test('does not create any profile directories', () => { // Act mockCreateOpenCodeStructure(); // Assert - OpenCode profile should not create any directories // Only the temp directory creation calls should exist const mkdirCalls = fs.mkdirSync.mock.calls.filter( (call) => !call[0].includes('task-master-test-') ); expect(mkdirCalls).toHaveLength(0); }); test('handles transformation of MCP config format', () => { // This test simulates the transformation behavior that would happen in onPostConvert const standardMcpConfig = { mcpServers: { 'taskmaster-ai': { command: 'node', args: ['path/to/server.js'], env: { API_KEY: 'test-key' } } } }; const expectedOpenCodeConfig = { $schema: 'https://opencode.ai/config.json', mcp: { 'taskmaster-ai': { type: 'local', command: ['node', 'path/to/server.js'], enabled: true, environment: { API_KEY: 'test-key' } } } }; // Mock the transformation behavior fs.writeFileSync( path.join(tempDir, 'opencode.json'), JSON.stringify(expectedOpenCodeConfig, null, 2) ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, 'opencode.json'), JSON.stringify(expectedOpenCodeConfig, null, 2) ); }); }); ``` -------------------------------------------------------------------------------- /apps/cli/src/ui/components/next-task.component.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview Next task recommendation component * Displays detailed information about the recommended next task */ import chalk from 'chalk'; import boxen from 'boxen'; import type { Task } from '@tm/core/types'; /** * Next task display options */ export interface NextTaskDisplayOptions { id: string | number; title: string; priority?: string; status?: string; dependencies?: (string | number)[]; description?: string; } /** * Display the recommended next task section */ export function displayRecommendedNextTask( task: NextTaskDisplayOptions | undefined ): void { if (!task) { // If no task available, show a message console.log( boxen( chalk.yellow( 'No tasks available to work on. All tasks are either completed, blocked by dependencies, or in progress.' ), { padding: 1, borderStyle: 'round', borderColor: 'yellow', title: '⚠ NO TASKS AVAILABLE ⚠', titleAlignment: 'center' } ) ); return; } // Build the content for the next task box const content = []; // Task header with ID and title content.push( `🔥 ${chalk.hex('#FF8800').bold('Next Task to Work On:')} ${chalk.yellow(`#${task.id}`)}${chalk.hex('#FF8800').bold(` - ${task.title}`)}` ); content.push(''); // Priority and Status line const statusLine = []; if (task.priority) { const priorityColor = task.priority === 'high' ? chalk.red : task.priority === 'medium' ? chalk.yellow : chalk.gray; statusLine.push(`Priority: ${priorityColor.bold(task.priority)}`); } if (task.status) { const statusDisplay = task.status === 'pending' ? chalk.yellow('○ pending') : task.status === 'in-progress' ? chalk.blue('▶ in-progress') : chalk.gray(task.status); statusLine.push(`Status: ${statusDisplay}`); } content.push(statusLine.join(' ')); // Dependencies const depsDisplay = !task.dependencies || task.dependencies.length === 0 ? chalk.gray('None') : chalk.cyan(task.dependencies.join(', ')); content.push(`Dependencies: ${depsDisplay}`); // Description if available if (task.description) { content.push(''); content.push(`Description: ${chalk.white(task.description)}`); } // Action commands content.push(''); content.push( `${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}` ); content.push( `${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${task.id}`)}` ); // Display in a styled box with orange border console.log( boxen(content.join('\n'), { padding: 1, margin: { top: 1, bottom: 1 }, borderStyle: 'round', borderColor: '#FFA500', // Orange color title: chalk.hex('#FFA500')('⚡ RECOMMENDED NEXT TASK ⚡'), titleAlignment: 'center', width: process.stdout.columns * 0.97, fullscreen: false }) ); } /** * Get task description from the full task object */ export function getTaskDescription(task: Task): string | undefined { // Try to get description from the task // This could be from task.description or the first line of task.details if ('description' in task && task.description) { return task.description as string; } if ('details' in task && task.details) { // Take first sentence or line from details const details = task.details as string; const firstLine = details.split('\n')[0]; const firstSentence = firstLine.split('.')[0]; return firstSentence; } return undefined; } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/scope-down.js: -------------------------------------------------------------------------------- ```javascript /** * scope-down.js * Direct function implementation for scoping down task complexity */ import { scopeDownTask } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; /** * Direct function wrapper for scoping down task complexity with error handling. * * @param {Object} args - Command arguments * @param {string} args.id - Comma-separated list of task IDs to scope down * @param {string} [args.strength='regular'] - Strength level (light, regular, heavy) * @param {string} [args.prompt] - Custom prompt for scoping adjustments * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) * @param {boolean} [args.research=false] - Whether to use research capabilities for scoping * @param {string} args.projectRoot - Project root path * @param {string} [args.tag] - Tag for the task context (optional) * @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 scopeDownDirect(args, log, context = {}) { // Destructure expected args const { tasksJsonPath, id, strength = 'regular', prompt: customPrompt, research = false, projectRoot, tag } = args; const { session } = context; // Destructure session from context // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); // Create logger wrapper using the utility const mcpLog = createLogWrapper(log); try { // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('scopeDownDirect called without tasksJsonPath'); disableSilentMode(); // Disable before returning return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Check required parameters if (!id) { log.error('Missing required parameter: id'); disableSilentMode(); return { success: false, error: { code: 'MISSING_PARAMETER', message: 'The id parameter is required for scoping down tasks' } }; } // Parse task IDs - convert to numbers as expected by scopeDownTask const taskIds = id.split(',').map((taskId) => parseInt(taskId.trim(), 10)); log.info( `Scoping down tasks: ${taskIds.join(', ')}, strength: ${strength}, research: ${research}` ); // Call the scopeDownTask function const result = await scopeDownTask( tasksJsonPath, taskIds, strength, customPrompt, { session, mcpLog, projectRoot, commandName: 'scope-down', outputType: 'mcp', tag, research }, 'json' // outputFormat ); // Restore normal logging disableSilentMode(); return { success: true, data: { updatedTasks: result.updatedTasks, tasksUpdated: result.updatedTasks.length, message: `Successfully scoped down ${result.updatedTasks.length} task(s)`, telemetryData: result.telemetryData } }; } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error in scopeDownDirect: ${error.message}`); return { success: false, error: { code: error.code || 'SCOPE_DOWN_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/expand-all-tasks.js: -------------------------------------------------------------------------------- ```javascript /** * Direct function wrapper for expandAllTasks */ import { expandAllTasks } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; /** * Expand all pending tasks with subtasks (Direct Function Wrapper) * @param {Object} args - Function arguments * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. * @param {number|string} [args.num] - Number of subtasks to generate * @param {boolean} [args.research] - Enable research-backed subtask generation * @param {string} [args.prompt] - Additional context to guide subtask generation * @param {boolean} [args.force] - Force regeneration of subtasks for tasks that already have them * @param {string} [args.projectRoot] - Project root path. * @param {string} [args.tag] - Tag for the task (optional) * @param {Object} log - Logger object from FastMCP * @param {Object} context - Context object containing session * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>} */ export async function expandAllTasksDirect(args, log, context = {}) { const { session } = context; // Extract session // Destructure expected args, including projectRoot const { tasksJsonPath, num, research, prompt, force, projectRoot, tag } = args; // Create logger wrapper using the utility const mcpLog = createLogWrapper(log); if (!tasksJsonPath) { log.error('expandAllTasksDirect called without tasksJsonPath'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } enableSilentMode(); // Enable silent mode for the core function call try { log.info( `Calling core expandAllTasks with args: ${JSON.stringify({ num, research, prompt, force, projectRoot, tag })}` ); // Parse parameters (ensure correct types) const numSubtasks = num ? parseInt(num, 10) : undefined; const useResearch = research === true; const additionalContext = prompt || ''; const forceFlag = force === true; // Call the core function, passing options and the context object { session, mcpLog, projectRoot } const result = await expandAllTasks( tasksJsonPath, numSubtasks, useResearch, additionalContext, forceFlag, { session, mcpLog, projectRoot, tag }, 'json' ); // Core function now returns a summary object including the *aggregated* telemetryData return { success: true, data: { message: `Expand all operation completed. Expanded: ${result.expandedCount}, Failed: ${result.failedCount}, Skipped: ${result.skippedCount}`, details: { expandedCount: result.expandedCount, failedCount: result.failedCount, skippedCount: result.skippedCount, tasksToExpand: result.tasksToExpand }, telemetryData: result.telemetryData // Pass the aggregated object } }; } catch (error) { // Log the error using the MCP logger log.error(`Error during core expandAllTasks execution: ${error.message}`); // Optionally log stack trace if available and debug enabled // if (error.stack && log.debug) { log.debug(error.stack); } return { success: false, error: { code: 'CORE_FUNCTION_ERROR', // Or a more specific code if possible message: error.message } }; } finally { disableSilentMode(); // IMPORTANT: Ensure silent mode is always disabled } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/list-tags.js: -------------------------------------------------------------------------------- ```javascript /** * list-tags.js * Direct function implementation for listing all tags */ import { tags } from '../../../../scripts/modules/task-manager/tag-management.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; /** * Direct function wrapper for listing all tags with error handling. * * @param {Object} args - Command arguments * @param {boolean} [args.showMetadata=false] - Whether to include metadata in the output * @param {string} [args.tasksJsonPath] - Path to the tasks.json file (resolved by tool) * @param {string} [args.projectRoot] - Project root path * @param {Object} log - Logger object * @param {Object} context - Additional context (session) * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } } */ export async function listTagsDirect(args, log, context = {}) { // Destructure expected args const { tasksJsonPath, showMetadata = false, projectRoot } = args; const { session } = context; // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); // Create logger wrapper using the utility const mcpLog = createLogWrapper(log); try { // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('listTagsDirect called without tasksJsonPath'); disableSilentMode(); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } log.info('Listing all tags'); // Prepare options const options = { showMetadata }; // Call the tags function const result = await tags( tasksJsonPath, options, { session, mcpLog, projectRoot }, 'json' // outputFormat - use 'json' to suppress CLI UI ); // Transform the result to remove full task data and provide summary info const tagsSummary = result.tags.map((tag) => { const tasks = tag.tasks || []; // Calculate status breakdown const statusBreakdown = tasks.reduce((acc, task) => { const status = task.status || 'pending'; acc[status] = (acc[status] || 0) + 1; return acc; }, {}); // Calculate subtask counts const subtaskCounts = tasks.reduce( (acc, task) => { if (task.subtasks && task.subtasks.length > 0) { acc.totalSubtasks += task.subtasks.length; task.subtasks.forEach((subtask) => { const subStatus = subtask.status || 'pending'; acc.subtasksByStatus[subStatus] = (acc.subtasksByStatus[subStatus] || 0) + 1; }); } return acc; }, { totalSubtasks: 0, subtasksByStatus: {} } ); return { name: tag.name, isCurrent: tag.isCurrent, taskCount: tasks.length, completedTasks: tag.completedTasks, statusBreakdown, subtaskCounts, created: tag.created, description: tag.description }; }); // Restore normal logging disableSilentMode(); return { success: true, data: { tags: tagsSummary, currentTag: result.currentTag, totalTags: result.totalTags, message: `Found ${result.totalTags} tag(s)` } }; } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error in listTagsDirect: ${error.message}`); return { success: false, error: { code: error.code || 'LIST_TAGS_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/set-task-status.js: -------------------------------------------------------------------------------- ```javascript /** * tools/setTaskStatus.js * Tool to set the status of a task */ import { z } from 'zod'; import { handleApiResult, createErrorResponse, withNormalizedProjectRoot } from './utils.js'; import { setTaskStatusDirect, nextTaskDirect } from '../core/task-master-core.js'; import { findTasksPath, findComplexityReportPath } from '../core/utils/path-utils.js'; import { TASK_STATUS_OPTIONS } from '../../../src/constants/task-status.js'; import { resolveTag } from '../../../scripts/modules/utils.js'; /** * Register the setTaskStatus tool with the MCP server * @param {Object} server - FastMCP server instance */ export function registerSetTaskStatusTool(server) { server.addTool({ name: 'set_task_status', description: 'Set the status of one or more tasks or subtasks.', parameters: z.object({ id: z .string() .describe( "Task ID or subtask ID (e.g., '15', '15.2'). Can be comma-separated to update multiple tasks/subtasks at once." ), status: z .enum(TASK_STATUS_OPTIONS) .describe( "New status to set (e.g., 'pending', 'done', 'in-progress', 'review', 'deferred', 'cancelled'." ), file: z.string().optional().describe('Absolute path to the tasks file'), complexityReport: z .string() .optional() .describe( 'Path to the complexity report file (relative to project root or absolute)' ), projectRoot: z .string() .describe('The directory of the project. Must be an absolute path.'), tag: z.string().optional().describe('Optional tag context to operate on') }), execute: withNormalizedProjectRoot(async (args, { log, session }) => { try { log.info( `Setting status of task(s) ${args.id} to: ${args.status} ${ args.tag ? `in tag: ${args.tag}` : 'in current tag' }` ); const resolvedTag = resolveTag({ projectRoot: args.projectRoot, tag: args.tag }); // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) let tasksJsonPath; try { tasksJsonPath = findTasksPath( { projectRoot: args.projectRoot, file: args.file }, log ); } catch (error) { log.error(`Error finding tasks.json: ${error.message}`); return createErrorResponse( `Failed to find tasks.json: ${error.message}` ); } let complexityReportPath; try { complexityReportPath = findComplexityReportPath( { projectRoot: args.projectRoot, complexityReport: args.complexityReport, tag: resolvedTag }, log ); } catch (error) { log.error(`Error finding complexity report: ${error.message}`); } const result = await setTaskStatusDirect( { tasksJsonPath: tasksJsonPath, id: args.id, status: args.status, complexityReportPath, projectRoot: args.projectRoot, tag: resolvedTag }, log, { session } ); if (result.success) { log.info( `Successfully updated status for task(s) ${args.id} to "${args.status}": ${result.data.message}` ); } else { log.error( `Failed to update task status: ${result.error?.message || 'Unknown error'}` ); } return handleApiResult( result, log, 'Error setting task status', undefined, args.projectRoot ); } catch (error) { log.error(`Error in setTaskStatus tool: ${error.message}`); return createErrorResponse( `Error setting task status: ${error.message}` ); } }) }); } ``` -------------------------------------------------------------------------------- /tests/unit/profiles/claude-integration.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; import fs from 'fs'; import path from 'path'; import os from 'os'; import { claudeProfile } from '../../../src/profiles/claude.js'; // Mock external modules jest.mock('child_process', () => ({ execSync: jest.fn() })); // Mock console methods jest.mock('console', () => ({ log: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), clear: jest.fn() })); describe('Claude Profile Integration', () => { let tempDir; beforeEach(() => { jest.clearAllMocks(); // Create a temporary directory for testing tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); // Spy on fs methods jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { if (filePath.toString().includes('AGENTS.md')) { return 'Sample AGENTS.md content for Claude integration'; } return '{}'; }); jest.spyOn(fs, 'existsSync').mockImplementation(() => false); jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); }); afterEach(() => { // Clean up the temporary directory try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch (err) { console.error(`Error cleaning up: ${err.message}`); } }); // Test function that simulates the Claude profile file copying behavior function mockCreateClaudeStructure() { // Claude profile copies AGENTS.md to CLAUDE.md in project root const sourceContent = 'Sample AGENTS.md content for Claude integration'; fs.writeFileSync(path.join(tempDir, 'CLAUDE.md'), sourceContent); } test('creates CLAUDE.md file in project root', () => { // Act mockCreateClaudeStructure(); // Assert expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, 'CLAUDE.md'), 'Sample AGENTS.md content for Claude integration' ); }); test('does not create any profile directories', () => { // Act mockCreateClaudeStructure(); // Assert - Claude profile should not create any directories // Only the temp directory creation calls should exist const mkdirCalls = fs.mkdirSync.mock.calls.filter( (call) => !call[0].includes('task-master-test-') ); expect(mkdirCalls).toHaveLength(0); }); test('supports MCP configuration when using rule transformer', () => { // This test verifies that the Claude profile is configured to support MCP // The actual MCP file creation is handled by the rule transformer // Assert - Claude profile should now support MCP configuration expect(claudeProfile.mcpConfig).toBe(true); expect(claudeProfile.mcpConfigName).toBe('.mcp.json'); expect(claudeProfile.mcpConfigPath).toBe('.mcp.json'); }); test('mock function does not create MCP configuration files', () => { // Act mockCreateClaudeStructure(); // Assert - The mock function should not create MCP config files // (This is expected since the mock doesn't use the rule transformer) const writeFileCalls = fs.writeFileSync.mock.calls; const mcpConfigCalls = writeFileCalls.filter( (call) => call[0].toString().includes('mcp.json') || call[0].toString().includes('mcp_settings.json') ); expect(mcpConfigCalls).toHaveLength(0); }); test('only creates the target integration guide file', () => { // Act mockCreateClaudeStructure(); // Assert - Should only create CLAUDE.md const writeFileCalls = fs.writeFileSync.mock.calls; expect(writeFileCalls).toHaveLength(1); expect(writeFileCalls[0][0]).toBe(path.join(tempDir, 'CLAUDE.md')); }); }); ``` -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/grok-cli/message-converter.js: -------------------------------------------------------------------------------- ```javascript /** * @fileoverview Message format conversion utilities for Grok CLI provider */ /** * @typedef {import('./types.js').GrokCliMessage} GrokCliMessage */ /** * Convert AI SDK messages to Grok CLI compatible format * @param {Array<Object>} messages - AI SDK message array * @returns {Array<GrokCliMessage>} Grok CLI compatible messages */ export function convertToGrokCliMessages(messages) { return messages.map((message) => { // Handle different message content types let content = ''; if (typeof message.content === 'string') { content = message.content; } else if (Array.isArray(message.content)) { // Handle multi-part content (text and images) content = message.content .filter((part) => part.type === 'text') .map((part) => part.text) .join('\n'); } else if (message.content && typeof message.content === 'object') { // Handle object content content = message.content.text || JSON.stringify(message.content); } return { role: message.role, content: content.trim() }; }); } /** * Convert Grok CLI response to AI SDK format * @param {string} responseText - Raw response text from Grok CLI (JSONL format) * @returns {Object} AI SDK compatible response object */ export function convertFromGrokCliResponse(responseText) { try { // Grok CLI outputs JSONL format - each line is a separate JSON message const lines = responseText .trim() .split('\n') .filter((line) => line.trim()); // Parse each line as JSON and find assistant messages const messages = []; for (const line of lines) { try { const message = JSON.parse(line); messages.push(message); } catch (parseError) { // Skip invalid JSON lines continue; } } // Find the last assistant message const assistantMessage = messages .filter((msg) => msg.role === 'assistant') .pop(); if (assistantMessage && assistantMessage.content) { return { text: assistantMessage.content, usage: assistantMessage.usage ? { promptTokens: assistantMessage.usage.prompt_tokens || 0, completionTokens: assistantMessage.usage.completion_tokens || 0, totalTokens: assistantMessage.usage.total_tokens || 0 } : undefined }; } // Fallback: if no assistant message found, return the raw text return { text: responseText.trim(), usage: undefined }; } catch (error) { // If parsing fails completely, treat as plain text response return { text: responseText.trim(), usage: undefined }; } } /** * Create a prompt string for Grok CLI from messages * @param {Array<Object>} messages - AI SDK message array * @returns {string} Formatted prompt string */ export function createPromptFromMessages(messages) { const grokMessages = convertToGrokCliMessages(messages); // Create a conversation-style prompt const prompt = grokMessages .map((message) => { switch (message.role) { case 'system': return `System: ${message.content}`; case 'user': return `User: ${message.content}`; case 'assistant': return `Assistant: ${message.content}`; default: return `${message.role}: ${message.content}`; } }) .join('\n\n'); return prompt; } /** * Escape shell arguments for safe CLI execution * @param {string} arg - Argument to escape * @returns {string} Shell-escaped argument */ export function escapeShellArg(arg) { if (typeof arg !== 'string') { arg = String(arg); } // Replace single quotes with '\'' return "'" + arg.replace(/'/g, "'\\''") + "'"; } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/executors/claude-executor.ts: -------------------------------------------------------------------------------- ```typescript /** * Claude executor implementation for Task Master */ import { spawn } from 'child_process'; import type { Task } from '../types/index.js'; import type { ExecutorType, ExecutionResult, ClaudeExecutorConfig } from './types.js'; import { BaseExecutor } from './base-executor.js'; export class ClaudeExecutor extends BaseExecutor { private claudeConfig: ClaudeExecutorConfig; private currentProcess: any = null; constructor(projectRoot: string, config: ClaudeExecutorConfig = {}) { super(projectRoot, config); this.claudeConfig = { command: config.command || 'claude', systemPrompt: config.systemPrompt || 'You are a helpful AI assistant helping to complete a software development task.', additionalFlags: config.additionalFlags || [] }; } getType(): ExecutorType { return 'claude'; } async isAvailable(): Promise<boolean> { return new Promise((resolve) => { const checkProcess = spawn('which', [this.claudeConfig.command!], { shell: true }); checkProcess.on('close', (code) => { resolve(code === 0); }); checkProcess.on('error', () => { resolve(false); }); }); } async execute(task: Task): Promise<ExecutionResult> { const startTime = new Date().toISOString(); try { // Check if Claude is available const isAvailable = await this.isAvailable(); if (!isAvailable) { return this.createResult( task.id, false, undefined, `Claude CLI not found. Please ensure 'claude' command is available in PATH.` ); } // Format the task into a prompt const taskPrompt = this.formatTaskPrompt(task); const fullPrompt = `${this.claudeConfig.systemPrompt}\n\nHere is the task to complete:\n\n${taskPrompt}`; // Execute Claude with the task details const result = await this.runClaude(fullPrompt, task.id); return { ...result, startTime, endTime: new Date().toISOString() }; } catch (error: any) { this.logger.error(`Failed to execute task ${task.id}:`, error); return this.createResult( task.id, false, undefined, error.message || 'Unknown error occurred' ); } } private runClaude(prompt: string, taskId: string): Promise<ExecutionResult> { return new Promise((resolve) => { const args = [prompt, ...this.claudeConfig.additionalFlags!]; this.logger.info(`Executing Claude for task ${taskId}`); this.logger.debug( `Command: ${this.claudeConfig.command} ${args.join(' ')}` ); this.currentProcess = spawn(this.claudeConfig.command!, args, { cwd: this.projectRoot, shell: false, stdio: 'inherit' // Let Claude handle its own I/O }); this.currentProcess.on('close', (code: number) => { this.currentProcess = null; if (code === 0) { resolve( this.createResult( taskId, true, 'Claude session completed successfully' ) ); } else { resolve( this.createResult( taskId, false, undefined, `Claude exited with code ${code}` ) ); } }); this.currentProcess.on('error', (error: any) => { this.currentProcess = null; this.logger.error(`Claude process error:`, error); resolve( this.createResult( taskId, false, undefined, `Failed to spawn Claude: ${error.message}` ) ); }); }); } async stop(): Promise<void> { if (this.currentProcess) { this.logger.info('Stopping Claude process...'); this.currentProcess.kill('SIGTERM'); this.currentProcess = null; } } } ``` -------------------------------------------------------------------------------- /tests/integration/profiles/roo-init-functionality.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; import fs from 'fs'; import path from 'path'; import { rooProfile } from '../../../src/profiles/roo.js'; import { COMMON_TOOL_MAPPINGS } from '../../../src/profiles/base-profile.js'; describe('Roo Profile Initialization Functionality', () => { let rooProfileContent; beforeAll(() => { // Read the roo.js profile file content once for all tests const rooJsPath = path.join(process.cwd(), 'src', 'profiles', 'roo.js'); rooProfileContent = fs.readFileSync(rooJsPath, 'utf8'); }); test('roo.js uses factory pattern with correct configuration', () => { // Check for explicit, non-default values in the source file expect(rooProfileContent).toContain("name: 'roo'"); expect(rooProfileContent).toContain("displayName: 'Roo Code'"); expect(rooProfileContent).toContain( 'toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE' ); // Check the final computed properties on the profile object expect(rooProfile.profileName).toBe('roo'); expect(rooProfile.displayName).toBe('Roo Code'); expect(rooProfile.profileDir).toBe('.roo'); // default expect(rooProfile.rulesDir).toBe('.roo/rules'); // default expect(rooProfile.mcpConfig).toBe(true); // default }); test('roo.js uses custom ROO_STYLE tool mappings', () => { // Check that the profile uses the correct, non-standard tool mappings expect(rooProfileContent).toContain( 'toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE' ); // Verify the result: roo uses custom tool names expect(rooProfile.conversionConfig.toolNames.edit_file).toBe('apply_diff'); expect(rooProfile.conversionConfig.toolNames.search).toBe('search_files'); }); test('roo.js profile ensures Roo directory structure via onAddRulesProfile', () => { // Check if onAddRulesProfile function exists expect(rooProfileContent).toContain( 'onAddRulesProfile(targetDir, assetsDir)' ); // Check for the general copy of assets/roocode which includes .roo base structure expect(rooProfileContent).toContain( "const sourceDir = path.join(assetsDir, 'roocode');" ); expect(rooProfileContent).toContain( 'copyRecursiveSync(sourceDir, targetDir);' ); // Check for the specific .roo modes directory handling expect(rooProfileContent).toContain( "const rooModesDir = path.join(sourceDir, '.roo');" ); // Check for import of ROO_MODES from profiles.js instead of local definition expect(rooProfileContent).toContain( "import { ROO_MODES } from '../constants/profiles.js';" ); }); test('roo.js profile copies .roomodes file via onAddRulesProfile', () => { expect(rooProfileContent).toContain( 'onAddRulesProfile(targetDir, assetsDir)' ); // Check for the specific .roomodes copy logic expect(rooProfileContent).toContain( "const roomodesSrc = path.join(sourceDir, '.roomodes');" ); expect(rooProfileContent).toContain( "const roomodesDest = path.join(targetDir, '.roomodes');" ); expect(rooProfileContent).toContain( 'fs.copyFileSync(roomodesSrc, roomodesDest);' ); }); test('roo.js profile copies mode-specific rule files via onAddRulesProfile', () => { expect(rooProfileContent).toContain( 'onAddRulesProfile(targetDir, assetsDir)' ); expect(rooProfileContent).toContain('for (const mode of ROO_MODES)'); // Check for the specific mode rule file copy logic expect(rooProfileContent).toContain( 'const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`);' ); expect(rooProfileContent).toContain( "const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`);" ); }); }); ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/models.js: -------------------------------------------------------------------------------- ```javascript /** * models.js * Direct function for managing AI model configurations via MCP */ import { getModelConfiguration, getAvailableModelsList, setModel } from '../../../../scripts/modules/task-manager/models.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; import { CUSTOM_PROVIDERS_ARRAY } from '../../../../src/constants/providers.js'; // Define supported roles for model setting const MODEL_ROLES = ['main', 'research', 'fallback']; /** * Determine provider hint from custom provider flags * @param {Object} args - Arguments containing provider flags * @returns {string|undefined} Provider hint or undefined if no custom provider flag is set */ function getProviderHint(args) { return CUSTOM_PROVIDERS_ARRAY.find((provider) => args[provider]); } /** * Handle setting models for different roles * @param {Object} args - Arguments containing role-specific model IDs * @param {Object} context - Context object with session, mcpLog, projectRoot * @returns {Object|null} Result if a model was set, null if no model setting was requested */ async function handleModelSetting(args, context) { for (const role of MODEL_ROLES) { const roleKey = `set${role.charAt(0).toUpperCase() + role.slice(1)}`; // setMain, setResearch, setFallback if (args[roleKey]) { const providerHint = getProviderHint(args); return await setModel(role, args[roleKey], { ...context, providerHint }); } } return null; // No model setting was requested } /** * Get or update model configuration * @param {Object} args - Arguments passed by the MCP tool * @param {Object} log - MCP logger * @param {Object} context - MCP context (contains session) * @returns {Object} Result object with success, data/error fields */ export async function modelsDirect(args, log, context = {}) { const { session } = context; const { projectRoot } = args; // Extract projectRoot from args // Create a logger wrapper that the core functions can use const mcpLog = createLogWrapper(log); log.info(`Executing models_direct with args: ${JSON.stringify(args)}`); log.info(`Using project root: ${projectRoot}`); // Validate flags: only one custom provider flag can be used simultaneously const customProviderFlags = CUSTOM_PROVIDERS_ARRAY.filter( (provider) => args[provider] ); if (customProviderFlags.length > 1) { log.error( 'Error: Cannot use multiple custom provider flags simultaneously.' ); return { success: false, error: { code: 'INVALID_ARGS', message: 'Cannot use multiple custom provider flags simultaneously. Choose only one: openrouter, ollama, bedrock, azure, or vertex.' } }; } try { enableSilentMode(); try { // Check for the listAvailableModels flag if (args.listAvailableModels === true) { return await getAvailableModelsList({ session, mcpLog, projectRoot }); } // Handle setting any model role using unified function const modelContext = { session, mcpLog, projectRoot }; const modelSetResult = await handleModelSetting(args, modelContext); if (modelSetResult) { return modelSetResult; } // Default action: get current configuration return await getModelConfiguration({ session, mcpLog, projectRoot }); } finally { disableSilentMode(); } } catch (error) { log.error(`Error in models_direct: ${error.message}`); return { success: false, error: { code: 'DIRECT_FUNCTION_ERROR', message: error.message, details: error.stack } }; } } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/config/services/runtime-state-manager.service.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview Runtime State Manager Service * Manages runtime state separate from configuration */ import { promises as fs } from 'node:fs'; import path from 'node:path'; import { ERROR_CODES, TaskMasterError } from '../../errors/task-master-error.js'; import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; /** * Runtime state data structure */ export interface RuntimeState { /** Currently active tag */ currentTag: string; /** Last updated timestamp */ lastUpdated?: string; /** Additional metadata */ metadata?: Record<string, unknown>; } /** * RuntimeStateManager handles runtime state persistence * Single responsibility: Runtime state management (separate from config) */ export class RuntimeStateManager { private stateFilePath: string; private currentState: RuntimeState; constructor(projectRoot: string) { this.stateFilePath = path.join(projectRoot, '.taskmaster', 'state.json'); this.currentState = { currentTag: DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG }; } /** * Load runtime state from disk */ async loadState(): Promise<RuntimeState> { try { const stateData = await fs.readFile(this.stateFilePath, 'utf-8'); const rawState = JSON.parse(stateData); // Map legacy field names to current interface const state: RuntimeState = { currentTag: rawState.currentTag || rawState.activeTag || DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG, lastUpdated: rawState.lastUpdated, metadata: rawState.metadata }; // Apply environment variable override for current tag if (process.env.TASKMASTER_TAG) { state.currentTag = process.env.TASKMASTER_TAG; } this.currentState = state; return state; } catch (error: any) { if (error.code === 'ENOENT') { // State file doesn't exist, use defaults console.debug('No state.json found, using default state'); // Check environment variable if (process.env.TASKMASTER_TAG) { this.currentState.currentTag = process.env.TASKMASTER_TAG; } return this.currentState; } console.warn('Failed to load state file:', error.message); return this.currentState; } } /** * Save runtime state to disk */ async saveState(): Promise<void> { const stateDir = path.dirname(this.stateFilePath); try { await fs.mkdir(stateDir, { recursive: true }); const stateToSave = { ...this.currentState, lastUpdated: new Date().toISOString() }; await fs.writeFile( this.stateFilePath, JSON.stringify(stateToSave, null, 2), 'utf-8' ); } catch (error) { throw new TaskMasterError( 'Failed to save runtime state', ERROR_CODES.CONFIG_ERROR, { statePath: this.stateFilePath }, error as Error ); } } /** * Get the currently active tag */ getCurrentTag(): string { return this.currentState.currentTag; } /** * Set the current tag */ async setCurrentTag(tag: string): Promise<void> { this.currentState.currentTag = tag; await this.saveState(); } /** * Get current state */ getState(): RuntimeState { return { ...this.currentState }; } /** * Update metadata */ async updateMetadata(metadata: Record<string, unknown>): Promise<void> { this.currentState.metadata = { ...this.currentState.metadata, ...metadata }; await this.saveState(); } /** * Clear state file */ async clearState(): Promise<void> { try { await fs.unlink(this.stateFilePath); } catch (error: any) { if (error.code !== 'ENOENT') { throw error; } } this.currentState = { currentTag: DEFAULT_CONFIG_VALUES.TAGS.DEFAULT_TAG }; } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/update-tasks.js: -------------------------------------------------------------------------------- ```javascript /** * update-tasks.js * Direct function implementation for updating tasks based on new context */ import path from 'path'; import { updateTasks } from '../../../../scripts/modules/task-manager.js'; import { createLogWrapper } from '../../tools/utils.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; /** * Direct function wrapper for updating tasks based on new context. * * @param {Object} args - Command arguments containing projectRoot, from, prompt, research options. * @param {string} args.from - The ID of the task to update. * @param {string} args.prompt - The prompt to update the task with. * @param {boolean} args.research - Whether to use research mode. * @param {string} args.tasksJsonPath - Path to the tasks.json file. * @param {string} args.projectRoot - Project root path (for MCP/env fallback) * @param {string} args.tag - Tag for the task (optional) * @param {Object} log - Logger object. * @param {Object} context - Context object containing session data. * @returns {Promise<Object>} - Result object with success status and data/error information. */ export async function updateTasksDirect(args, log, context = {}) { const { session } = context; const { from, prompt, research, tasksJsonPath, projectRoot, tag } = args; // Create the standard logger wrapper const logWrapper = createLogWrapper(log); // --- Input Validation --- if (!projectRoot) { logWrapper.error('updateTasksDirect requires a projectRoot argument.'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'projectRoot is required.' } }; } if (!from) { logWrapper.error('updateTasksDirect called without from ID'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'Starting task ID (from) is required' } }; } if (!prompt) { logWrapper.error('updateTasksDirect called without prompt'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'Update prompt is required' } }; } logWrapper.info( `Updating tasks via direct function. From: ${from}, Research: ${research}, File: ${tasksJsonPath}, ProjectRoot: ${projectRoot}` ); enableSilentMode(); // Enable silent mode try { // Call the core updateTasks function const result = await updateTasks( tasksJsonPath, from, prompt, research, { session, mcpLog: logWrapper, projectRoot, tag }, 'json' ); if (result && result.success && Array.isArray(result.updatedTasks)) { logWrapper.success( `Successfully updated ${result.updatedTasks.length} tasks.` ); return { success: true, data: { message: `Successfully updated ${result.updatedTasks.length} tasks.`, tasksPath: tasksJsonPath, updatedCount: result.updatedTasks.length, telemetryData: result.telemetryData, tagInfo: result.tagInfo } }; } else { // Handle case where core function didn't return expected success structure logWrapper.error( 'Core updateTasks function did not return a successful structure.' ); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: result?.message || 'Core function failed to update tasks or returned unexpected result.' } }; } } catch (error) { logWrapper.error(`Error executing core updateTasks: ${error.message}`); return { success: false, error: { code: 'UPDATE_TASKS_CORE_ERROR', message: error.message || 'Unknown error updating tasks' } }; } finally { disableSilentMode(); // Ensure silent mode is disabled } } ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/clear-subtasks.js: -------------------------------------------------------------------------------- ```javascript import path from 'path'; import chalk from 'chalk'; import boxen from 'boxen'; import Table from 'cli-table3'; import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js'; import { displayBanner } from '../ui.js'; /** * Clear subtasks from specified tasks * @param {string} tasksPath - Path to the tasks.json file * @param {string} taskIds - Task IDs to clear subtasks from * @param {Object} context - Context object containing projectRoot and tag * @param {string} [context.projectRoot] - Project root path * @param {string} [context.tag] - Tag for the task */ function clearSubtasks(tasksPath, taskIds, context = {}) { const { projectRoot, tag } = context; log('info', `Reading tasks from ${tasksPath}...`); const data = readJSON(tasksPath, projectRoot, tag); if (!data || !data.tasks) { log('error', 'No valid tasks found.'); process.exit(1); } if (!isSilentMode()) { console.log( boxen(chalk.white.bold('Clearing Subtasks'), { padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }) ); } // Handle multiple task IDs (comma-separated) const taskIdArray = taskIds.split(',').map((id) => id.trim()); let clearedCount = 0; // Create a summary table for the cleared subtasks const summaryTable = new Table({ head: [ chalk.cyan.bold('Task ID'), chalk.cyan.bold('Task Title'), chalk.cyan.bold('Subtasks Cleared') ], colWidths: [10, 50, 20], style: { head: [], border: [] } }); taskIdArray.forEach((taskId) => { const id = parseInt(taskId, 10); if (Number.isNaN(id)) { log('error', `Invalid task ID: ${taskId}`); return; } const task = data.tasks.find((t) => t.id === id); if (!task) { log('error', `Task ${id} not found`); return; } if (!task.subtasks || task.subtasks.length === 0) { log('info', `Task ${id} has no subtasks to clear`); summaryTable.push([ id.toString(), truncate(task.title, 47), chalk.yellow('No subtasks') ]); return; } const subtaskCount = task.subtasks.length; task.subtasks = []; clearedCount++; log('info', `Cleared ${subtaskCount} subtasks from task ${id}`); summaryTable.push([ id.toString(), truncate(task.title, 47), chalk.green(`${subtaskCount} subtasks cleared`) ]); }); if (clearedCount > 0) { writeJSON(tasksPath, data, projectRoot, tag); // Show summary table if (!isSilentMode()) { console.log( boxen(chalk.white.bold('Subtask Clearing Summary:'), { padding: { left: 2, right: 2, top: 0, bottom: 0 }, margin: { top: 1, bottom: 0 }, borderColor: 'blue', borderStyle: 'round' }) ); console.log(summaryTable.toString()); } // Success message if (!isSilentMode()) { console.log( boxen( chalk.green( `Successfully cleared subtasks from ${chalk.bold(clearedCount)} task(s)` ), { padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } } ) ); // Next steps suggestion console.log( boxen( chalk.white.bold('Next Steps:') + '\n\n' + `${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --id=<id>')} to generate new subtasks\n` + `${chalk.cyan('2.')} Run ${chalk.yellow('task-master list --with-subtasks')} to verify changes`, { padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1 } } ) ); } } else { if (!isSilentMode()) { console.log( boxen(chalk.yellow('No subtasks were cleared'), { padding: 1, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1 } }) ); } } } export default clearSubtasks; ``` -------------------------------------------------------------------------------- /.taskmaster/docs/prd-tm-start.txt: -------------------------------------------------------------------------------- ``` <context> # Overview Add a new CLI command: `task-master start <task_id>` (alias: `tm start <task_id>`). This command hard-codes `claude-code` as the executor, fetches task details, builds a standardized prompt, runs claude-code, shows the result, checks for git changes, and auto-marks the task as done if successful. We follow the Commander class pattern, reuse task retrieval from `show` command flow. Extremely minimal for 1-hour hackathon timeline. # Core Features - `start` command (Commander class style) - Hard-coded executor: `claude-code` - Standardized prompt designed for minimal changes following existing patterns - Shows claude-code output (no streaming) - Git status check for success detection - Auto-mark task done if successful # User Experience ``` task-master start 12 ``` 1) Fetches Task #12 details 2) Builds standardized prompt with task context 3) Runs claude-code with the prompt 4) Shows output 5) Checks git status for changes 6) Auto-marks task done if changes detected </context> <PRD> # Technical Architecture - Command pattern: - Create `apps/cli/src/commands/start.command.ts` modeled on [list.command.ts](mdc:apps/cli/src/commands/list.command.ts) and task lookup from [show.command.ts](mdc:apps/cli/src/commands/show.command.ts) - Task retrieval: - Use `@tm/core` via `createTaskMasterCore` to get task by ID - Extract: id, title, description, details - Executor (ultra-simple approach): - Execute `claude "full prompt here"` command directly - The prompt tells Claude to first run `tm show <task_id>` to get task details - Then tells Claude to implement the code changes - This opens Claude CLI interface naturally in the current terminal - No subprocess management needed - just execute the command - Execution flow: 1) Validate `<task_id>` exists; exit with error if not 2) Build standardized prompt that includes instructions to run `tm show <task_id>` 3) Execute `claude "prompt"` command directly in terminal 4) Claude CLI opens, runs `tm show`, then implements changes 5) After Claude session ends, run `git status --porcelain` to detect changes 6) If changes detected, auto-run `task-master set-status --id=<task_id> --status=done` - Success criteria: - Success = exit code 0 AND git shows modified/created files - Print changed file paths; warn if no changes detected # Development Roadmap MVP (ship in ~1 hour): 1) Implement `start.command.ts` (Commander class), parse `<task_id>` 2) Validate task exists via tm-core 3) Build prompt that tells Claude to run `tm show <task_id>` then implement 4) Execute `claude "prompt"` command, then check git status and auto-mark done # Risks and Mitigations - Executor availability: Error clearly if `claude-code` provider fails - False success: Git-change heuristic acceptable for hackathon MVP # Appendix **Standardized Prompt Template:** ``` You are an AI coding assistant with access to this repository's codebase. First, run this command to get the task details: tm show <task_id> Then implement the task with these requirements: - Make the SMALLEST number of code changes possible - Follow ALL existing patterns in the codebase (you have access to analyze the code) - Do NOT over-engineer the solution - Use existing files/functions/patterns wherever possible - When complete, print: COMPLETED: <brief summary of changes> Begin by running tm show <task_id> to understand what needs to be implemented. ``` **Key References:** - [list.command.ts](mdc:apps/cli/src/commands/list.command.ts) - Command structure - [show.command.ts](mdc:apps/cli/src/commands/show.command.ts) - Task validation - Node.js `child_process.exec()` - For executing `claude "prompt"` command </PRD> ``` -------------------------------------------------------------------------------- /packages/tm-core/src/subpath-exports.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Test file documenting subpath export usage * This demonstrates how consumers can use granular imports for better tree-shaking */ import { describe, it, expect } from 'vitest'; describe('Subpath Exports', () => { it('should allow importing from auth subpath', async () => { // Instead of: import { AuthManager } from '@tm/core'; // Use: import { AuthManager } from '@tm/core/auth'; const authModule = await import('./auth'); expect(authModule.AuthManager).toBeDefined(); expect(authModule.AuthenticationError).toBeDefined(); }); it('should allow importing from storage subpath', async () => { // Instead of: import { FileStorage } from '@tm/core'; // Use: import { FileStorage } from '@tm/core/storage'; const storageModule = await import('./storage'); expect(storageModule.FileStorage).toBeDefined(); expect(storageModule.ApiStorage).toBeDefined(); expect(storageModule.StorageFactory).toBeDefined(); }); it('should allow importing from config subpath', async () => { // Instead of: import { ConfigManager } from '@tm/core'; // Use: import { ConfigManager } from '@tm/core/config'; const configModule = await import('./config'); expect(configModule.ConfigManager).toBeDefined(); }); it('should allow importing from errors subpath', async () => { // Instead of: import { TaskMasterError } from '@tm/core'; // Use: import { TaskMasterError } from '@tm/core/errors'; const errorsModule = await import('./errors'); expect(errorsModule.TaskMasterError).toBeDefined(); expect(errorsModule.ERROR_CODES).toBeDefined(); }); it('should allow importing from logger subpath', async () => { // Instead of: import { getLogger } from '@tm/core'; // Use: import { getLogger } from '@tm/core/logger'; const loggerModule = await import('./logger'); expect(loggerModule.getLogger).toBeDefined(); expect(loggerModule.createLogger).toBeDefined(); }); it('should allow importing from providers subpath', async () => { // Instead of: import { BaseProvider } from '@tm/core'; // Use: import { BaseProvider } from '@tm/core/providers'; const providersModule = await import('./providers'); expect(providersModule.BaseProvider).toBeDefined(); }); it('should allow importing from services subpath', async () => { // Instead of: import { TaskService } from '@tm/core'; // Use: import { TaskService } from '@tm/core/services'; const servicesModule = await import('./services'); expect(servicesModule.TaskService).toBeDefined(); }); it('should allow importing from utils subpath', async () => { // Instead of: import { generateId } from '@tm/core'; // Use: import { generateId } from '@tm/core/utils'; const utilsModule = await import('./utils'); expect(utilsModule.generateId).toBeDefined(); }); }); /** * Usage Examples for Consumers: * * 1. Import only authentication (smaller bundle): * ```typescript * import { AuthManager, AuthenticationError } from '@tm/core/auth'; * ``` * * 2. Import only storage (no auth code bundled): * ```typescript * import { FileStorage, StorageFactory } from '@tm/core/storage'; * ``` * * 3. Import only errors (minimal bundle): * ```typescript * import { TaskMasterError, ERROR_CODES } from '@tm/core/errors'; * ``` * * 4. Still support convenience imports (larger bundle but better DX): * ```typescript * import { AuthManager, FileStorage, TaskMasterError } from '@tm/core'; * ``` * * Benefits: * - Better tree-shaking: unused modules are not bundled * - Clearer dependencies: explicit about what parts of the library you use * - Faster builds: bundlers can optimize better with granular imports * - Smaller bundles: especially important for browser/edge deployments */ ``` -------------------------------------------------------------------------------- /src/prompts/research.json: -------------------------------------------------------------------------------- ```json { "id": "research", "version": "1.0.0", "description": "Perform AI-powered research with project context", "metadata": { "author": "system", "created": "2024-01-01T00:00:00Z", "updated": "2024-01-01T00:00:00Z", "tags": ["research", "context-aware", "information-gathering"] }, "parameters": { "query": { "type": "string", "required": true, "description": "Research query" }, "gatheredContext": { "type": "string", "default": "", "description": "Gathered project context" }, "detailLevel": { "type": "string", "enum": ["low", "medium", "high"], "default": "medium", "description": "Level of detail for the response" }, "projectInfo": { "type": "object", "description": "Project information", "properties": { "root": { "type": "string", "description": "Project root path" }, "taskCount": { "type": "number", "description": "Number of related tasks" }, "fileCount": { "type": "number", "description": "Number of related files" } } } }, "prompts": { "default": { "system": "You are an expert AI research assistant helping with a software development project. You have access to project context including tasks, files, and project structure.\n\nYour role is to provide comprehensive, accurate, and actionable research responses based on the user's query and the provided project context.\n{{#if (eq detailLevel \"low\")}}\n**Response Style: Concise & Direct**\n- Provide brief, focused answers (2-4 paragraphs maximum)\n- Focus on the most essential information\n- Use bullet points for key takeaways\n- Avoid lengthy explanations unless critical\n- Skip pleasantries, introductions, and conclusions\n- No phrases like \"Based on your project context\" or \"I'll provide guidance\"\n- No summary outros or alignment statements\n- Get straight to the actionable information\n- Use simple, direct language - users want info, not explanation{{/if}}{{#if (eq detailLevel \"medium\")}}\n**Response Style: Balanced & Comprehensive**\n- Provide thorough but well-structured responses (4-8 paragraphs)\n- Include relevant examples and explanations\n- Balance depth with readability\n- Use headings and bullet points for organization{{/if}}{{#if (eq detailLevel \"high\")}}\n**Response Style: Detailed & Exhaustive**\n- Provide comprehensive, in-depth analysis (8+ paragraphs)\n- Include multiple perspectives and approaches\n- Provide detailed examples, code snippets, and step-by-step guidance\n- Cover edge cases and potential pitfalls\n- Use clear structure with headings, subheadings, and lists{{/if}}\n\n**Guidelines:**\n- Always consider the project context when formulating responses\n- Reference specific tasks, files, or project elements when relevant\n- Provide actionable insights that can be applied to the project\n- If the query relates to existing project tasks, suggest how the research applies to those tasks\n- Use markdown formatting for better readability\n- Be precise and avoid speculation unless clearly marked as such\n{{#if (eq detailLevel \"low\")}}\n**For LOW detail level specifically:**\n- Start immediately with the core information\n- No introductory phrases or context acknowledgments\n- No concluding summaries or project alignment statements\n- Focus purely on facts, steps, and actionable items{{/if}}", "user": "# Research Query\n\n{{query}}\n{{#if gatheredContext}}\n\n# Project Context\n\n{{gatheredContext}}\n{{/if}}\n\n# Instructions\n\nPlease research and provide a {{detailLevel}}-detail response to the query above. Consider the project context provided and make your response as relevant and actionable as possible for this specific project." } } } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/storage/file-storage/file-operations.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview File operations with atomic writes and locking */ import { promises as fs } from 'node:fs'; import type { FileStorageData } from './format-handler.js'; /** * Handles atomic file operations with locking mechanism */ export class FileOperations { private fileLocks: Map<string, Promise<void>> = new Map(); /** * Read and parse JSON file */ async readJson(filePath: string): Promise<any> { try { const content = await fs.readFile(filePath, 'utf-8'); return JSON.parse(content); } catch (error: any) { if (error.code === 'ENOENT') { throw error; // Re-throw ENOENT for caller to handle } if (error instanceof SyntaxError) { throw new Error(`Invalid JSON in file ${filePath}: ${error.message}`); } throw new Error(`Failed to read file ${filePath}: ${error.message}`); } } /** * Write JSON file with atomic operation and locking */ async writeJson( filePath: string, data: FileStorageData | any ): Promise<void> { // Use file locking to prevent concurrent writes const lockKey = filePath; const existingLock = this.fileLocks.get(lockKey); if (existingLock) { await existingLock; } const lockPromise = this.performAtomicWrite(filePath, data); this.fileLocks.set(lockKey, lockPromise); try { await lockPromise; } finally { this.fileLocks.delete(lockKey); } } /** * Perform atomic write operation using temporary file */ private async performAtomicWrite(filePath: string, data: any): Promise<void> { const tempPath = `${filePath}.tmp`; try { // Write to temp file first const content = JSON.stringify(data, null, 2); await fs.writeFile(tempPath, content, 'utf-8'); // Atomic rename await fs.rename(tempPath, filePath); } catch (error: any) { // Clean up temp file if it exists try { await fs.unlink(tempPath); } catch { // Ignore cleanup errors } throw new Error(`Failed to write file ${filePath}: ${error.message}`); } } /** * Check if file exists */ async exists(filePath: string): Promise<boolean> { try { await fs.access(filePath, fs.constants.F_OK); return true; } catch { return false; } } /** * Get file stats */ async getStats(filePath: string) { return fs.stat(filePath); } /** * Read directory contents */ async readDir(dirPath: string): Promise<string[]> { return fs.readdir(dirPath); } /** * Create directory recursively */ async ensureDir(dirPath: string): Promise<void> { try { await fs.mkdir(dirPath, { recursive: true }); } catch (error: any) { throw new Error( `Failed to create directory ${dirPath}: ${error.message}` ); } } /** * Delete file */ async deleteFile(filePath: string): Promise<void> { try { await fs.unlink(filePath); } catch (error: any) { if (error.code !== 'ENOENT') { throw new Error(`Failed to delete file ${filePath}: ${error.message}`); } } } /** * Rename/move file */ async moveFile(oldPath: string, newPath: string): Promise<void> { try { await fs.rename(oldPath, newPath); } catch (error: any) { throw new Error( `Failed to move file from ${oldPath} to ${newPath}: ${error.message}` ); } } /** * Copy file */ async copyFile(srcPath: string, destPath: string): Promise<void> { try { await fs.copyFile(srcPath, destPath); } catch (error: any) { throw new Error( `Failed to copy file from ${srcPath} to ${destPath}: ${error.message}` ); } } /** * Clean up all pending file operations */ async cleanup(): Promise<void> { const locks = Array.from(this.fileLocks.values()); if (locks.length > 0) { await Promise.all(locks); } this.fileLocks.clear(); } } ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/components/TagDropdown.tsx: -------------------------------------------------------------------------------- ```typescript import React, { useState, useEffect, useRef } from 'react'; interface TagDropdownProps { currentTag: string; availableTags: string[]; onTagSwitch: (tagName: string) => Promise<void>; sendMessage: (message: any) => Promise<any>; dispatch: React.Dispatch<any>; } export const TagDropdown: React.FC<TagDropdownProps> = ({ currentTag, availableTags, onTagSwitch, sendMessage, dispatch }) => { const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const dropdownRef = useRef<HTMLDivElement>(null); // Fetch tags when component mounts useEffect(() => { fetchTags(); }, []); // Handle click outside to close dropdown useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( dropdownRef.current && !dropdownRef.current.contains(event.target as Node) ) { setIsOpen(false); } }; if (isOpen) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [isOpen]); const fetchTags = async () => { try { const result = await sendMessage({ type: 'getTags' }); if (result?.tags && result?.currentTag) { const tagNames = result.tags.map((tag: any) => tag.name || tag); dispatch({ type: 'SET_TAG_DATA', payload: { currentTag: result.currentTag, availableTags: tagNames } }); } } catch (error) { console.error('Failed to fetch tags:', error); } }; const handleTagSwitch = async (tagName: string) => { if (tagName === currentTag) { setIsOpen(false); return; } setIsLoading(true); try { await onTagSwitch(tagName); dispatch({ type: 'SET_CURRENT_TAG', payload: tagName }); setIsOpen(false); } catch (error) { console.error('Failed to switch tag:', error); } finally { setIsLoading(false); } }; return ( <div className="relative" ref={dropdownRef}> <button onClick={() => setIsOpen(!isOpen)} disabled={isLoading} className="flex items-center gap-2 px-3 py-1.5 text-sm bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border rounded hover:bg-vscode-list-hoverBackground transition-colors" > <span className="font-medium">{currentTag}</span> <svg className={`w-4 h-4 transition-transform ${isOpen ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /> </svg> </button> {isOpen && ( <div className="absolute top-full mt-1 right-0 bg-background border border-vscode-dropdown-border rounded shadow-lg z-50 min-w-[200px] py-1"> {availableTags.map((tag) => ( <button key={tag} onClick={() => handleTagSwitch(tag)} className={`w-full text-left px-3 py-2 text-sm transition-colors flex items-center justify-between group ${ tag === currentTag ? 'bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground' : 'hover:bg-vscode-list-hoverBackground text-vscode-dropdown-foreground' }`} > <span className="truncate pr-2">{tag}</span> {tag === currentTag && ( <svg className="w-4 h-4 flex-shrink-0 text-vscode-textLink-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> </svg> )} </button> ))} </div> )} </div> ); }; ``` -------------------------------------------------------------------------------- /packages/tm-core/src/auth/auth-manager.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Tests for AuthManager singleton behavior */ import { describe, it, expect, beforeEach, vi } from 'vitest'; // Mock the logger to verify warnings (must be hoisted before SUT import) const mockLogger = { warn: vi.fn(), info: vi.fn(), debug: vi.fn(), error: vi.fn() }; vi.mock('../logger/index.js', () => ({ getLogger: () => mockLogger })); // Spy on CredentialStore constructor to verify config propagation const CredentialStoreSpy = vi.fn(); vi.mock('./credential-store.js', () => { return { CredentialStore: class { constructor(config: any) { CredentialStoreSpy(config); this.getCredentials = vi.fn(() => null); } getCredentials() { return null; } saveCredentials() {} clearCredentials() {} hasValidCredentials() { return false; } } }; }); // Mock OAuthService to avoid side effects vi.mock('./oauth-service.js', () => { return { OAuthService: class { constructor() {} authenticate() { return Promise.resolve({}); } getAuthorizationUrl() { return null; } } }; }); // Mock SupabaseAuthClient to avoid side effects vi.mock('../clients/supabase-client.js', () => { return { SupabaseAuthClient: class { constructor() {} refreshSession() { return Promise.resolve({}); } signOut() { return Promise.resolve(); } } }; }); // Import SUT after mocks import { AuthManager } from './auth-manager.js'; describe('AuthManager Singleton', () => { beforeEach(() => { // Reset singleton before each test AuthManager.resetInstance(); vi.clearAllMocks(); CredentialStoreSpy.mockClear(); }); it('should return the same instance on multiple calls', () => { const instance1 = AuthManager.getInstance(); const instance2 = AuthManager.getInstance(); expect(instance1).toBe(instance2); }); it('should use config on first call', () => { const config = { baseUrl: 'https://test.auth.com', configDir: '/test/config', configFile: '/test/config/auth.json' }; const instance = AuthManager.getInstance(config); expect(instance).toBeDefined(); // Assert that CredentialStore was constructed with the provided config expect(CredentialStoreSpy).toHaveBeenCalledTimes(1); expect(CredentialStoreSpy).toHaveBeenCalledWith(config); // Verify the config is passed to internal components through observable behavior // getCredentials would look in the configured file path const credentials = instance.getCredentials(); expect(credentials).toBeNull(); // File doesn't exist, but config was propagated correctly }); it('should warn when config is provided after initialization', () => { // Clear previous calls mockLogger.warn.mockClear(); // First call with config AuthManager.getInstance({ baseUrl: 'https://first.auth.com' }); // Second call with different config AuthManager.getInstance({ baseUrl: 'https://second.auth.com' }); // Verify warning was logged expect(mockLogger.warn).toHaveBeenCalledWith( expect.stringMatching(/config.*after initialization.*ignored/i) ); }); it('should not warn when no config is provided after initialization', () => { // Clear previous calls mockLogger.warn.mockClear(); // First call with config AuthManager.getInstance({ configDir: '/test/config' }); // Second call without config AuthManager.getInstance(); // Verify no warning was logged expect(mockLogger.warn).not.toHaveBeenCalled(); }); it('should allow resetting the instance', () => { const instance1 = AuthManager.getInstance(); // Reset the instance AuthManager.resetInstance(); // Get new instance const instance2 = AuthManager.getInstance(); // They should be different instances expect(instance1).not.toBe(instance2); }); }); ``` -------------------------------------------------------------------------------- /tests/integration/profiles/opencode-init-functionality.test.js: -------------------------------------------------------------------------------- ```javascript import fs from 'fs'; import path from 'path'; import { opencodeProfile } from '../../../src/profiles/opencode.js'; describe('OpenCode Profile Initialization Functionality', () => { let opencodeProfileContent; beforeAll(() => { const opencodeJsPath = path.join( process.cwd(), 'src', 'profiles', 'opencode.js' ); opencodeProfileContent = fs.readFileSync(opencodeJsPath, 'utf8'); }); test('opencode.js has correct asset-only profile configuration', () => { // Check for explicit, non-default values in the source file expect(opencodeProfileContent).toContain("name: 'opencode'"); expect(opencodeProfileContent).toContain("displayName: 'OpenCode'"); expect(opencodeProfileContent).toContain("url: 'opencode.ai'"); expect(opencodeProfileContent).toContain("docsUrl: 'opencode.ai/docs/'"); expect(opencodeProfileContent).toContain("profileDir: '.'"); // non-default expect(opencodeProfileContent).toContain("rulesDir: '.'"); // non-default expect(opencodeProfileContent).toContain("mcpConfigName: 'opencode.json'"); // non-default expect(opencodeProfileContent).toContain('includeDefaultRules: false'); // non-default expect(opencodeProfileContent).toContain("'AGENTS.md': 'AGENTS.md'"); // Check the final computed properties on the profile object expect(opencodeProfile.profileName).toBe('opencode'); expect(opencodeProfile.displayName).toBe('OpenCode'); expect(opencodeProfile.profileDir).toBe('.'); expect(opencodeProfile.rulesDir).toBe('.'); expect(opencodeProfile.mcpConfig).toBe(true); // computed from mcpConfigName expect(opencodeProfile.mcpConfigName).toBe('opencode.json'); expect(opencodeProfile.mcpConfigPath).toBe('opencode.json'); // computed expect(opencodeProfile.includeDefaultRules).toBe(false); expect(opencodeProfile.fileMap['AGENTS.md']).toBe('AGENTS.md'); }); test('opencode.js has lifecycle functions for MCP config transformation', () => { expect(opencodeProfileContent).toContain( 'function onPostConvertRulesProfile' ); expect(opencodeProfileContent).toContain('function onRemoveRulesProfile'); expect(opencodeProfileContent).toContain('transformToOpenCodeFormat'); }); test('opencode.js handles opencode.json transformation in lifecycle functions', () => { expect(opencodeProfileContent).toContain('opencode.json'); expect(opencodeProfileContent).toContain('transformToOpenCodeFormat'); expect(opencodeProfileContent).toContain('$schema'); expect(opencodeProfileContent).toContain('mcpServers'); expect(opencodeProfileContent).toContain('mcp'); }); test('opencode.js has proper error handling in lifecycle functions', () => { expect(opencodeProfileContent).toContain('try {'); expect(opencodeProfileContent).toContain('} catch (error) {'); expect(opencodeProfileContent).toContain('log('); }); test('opencode.js uses custom MCP config name', () => { // OpenCode uses opencode.json instead of mcp.json expect(opencodeProfileContent).toContain("mcpConfigName: 'opencode.json'"); // Should not contain mcp.json as a config value (comments are OK) expect(opencodeProfileContent).not.toMatch( /mcpConfigName:\s*['"]mcp\.json['"]/ ); }); test('opencode.js has transformation logic for OpenCode format', () => { // Check for transformation function expect(opencodeProfileContent).toContain('transformToOpenCodeFormat'); // Check for specific transformation logic expect(opencodeProfileContent).toContain('mcpServers'); expect(opencodeProfileContent).toContain('command'); expect(opencodeProfileContent).toContain('args'); expect(opencodeProfileContent).toContain('environment'); expect(opencodeProfileContent).toContain('enabled'); expect(opencodeProfileContent).toContain('type'); }); }); ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/remove-task.js: -------------------------------------------------------------------------------- ```javascript /** * remove-task.js * Direct function implementation for removing a task */ import { removeTask, taskExists } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, disableSilentMode, readJSON } from '../../../../scripts/modules/utils.js'; /** * Direct function wrapper for removeTask with error handling. * Supports removing multiple tasks at once with comma-separated IDs. * * @param {Object} args - Command arguments * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. * @param {string} args.id - The ID(s) of the task(s) or subtask(s) to remove (comma-separated for multiple). * @param {string} args.projectRoot - Project root path (for MCP/env fallback) * @param {string} args.tag - Tag for the task (optional) * @param {Object} log - Logger object * @returns {Promise<Object>} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string } } */ export async function removeTaskDirect(args, log, context = {}) { // Destructure expected args const { tasksJsonPath, id, projectRoot, tag } = args; const { session } = context; try { // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('removeTaskDirect called without tasksJsonPath'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Validate task ID parameter if (!id) { log.error('Task ID is required'); return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: 'Task ID is required' } }; } // Split task IDs if comma-separated const taskIdArray = id.split(',').map((taskId) => taskId.trim()); log.info( `Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}${tag ? ` in tag '${tag}'` : ''}` ); // Validate all task IDs exist before proceeding const data = readJSON(tasksJsonPath, projectRoot, tag); if (!data || !data.tasks) { return { success: false, error: { code: 'INVALID_TASKS_FILE', message: `No valid tasks found in ${tasksJsonPath}${tag ? ` for tag '${tag}'` : ''}` } }; } const invalidTasks = taskIdArray.filter( (taskId) => !taskExists(data.tasks, taskId) ); if (invalidTasks.length > 0) { return { success: false, error: { code: 'INVALID_TASK_ID', message: `The following tasks were not found${tag ? ` in tag '${tag}'` : ''}: ${invalidTasks.join(', ')}` } }; } // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); try { // Call removeTask with proper context including tag const result = await removeTask(tasksJsonPath, id, { projectRoot, tag }); if (!result.success) { return { success: false, error: { code: 'REMOVE_TASK_ERROR', message: result.error || 'Failed to remove tasks' } }; } log.info(`Successfully removed ${result.removedTasks.length} task(s)`); return { success: true, data: { totalTasks: taskIdArray.length, successful: result.removedTasks.length, failed: taskIdArray.length - result.removedTasks.length, removedTasks: result.removedTasks, message: result.message, tasksPath: tasksJsonPath, tag } }; } finally { // Restore normal logging disableSilentMode(); } } catch (error) { // Ensure silent mode is disabled even if an outer error occurs disableSilentMode(); // Catch any unexpected errors log.error(`Unexpected error in removeTaskDirect: ${error.message}`); return { success: false, error: { code: 'UNEXPECTED_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /packages/tm-core/tests/unit/smoke.test.ts: -------------------------------------------------------------------------------- ```typescript /** * Smoke tests to verify basic package functionality and imports */ import { PlaceholderParser, PlaceholderStorage, StorageError, TaskNotFoundError, TmCoreError, ValidationError, formatDate, generateTaskId, isValidTaskId, name, version } from '@tm/core'; import type { PlaceholderTask, TaskId, TaskPriority, TaskStatus } from '@tm/core'; describe('tm-core smoke tests', () => { describe('package metadata', () => { it('should export correct package name and version', () => { expect(name).toBe('@task-master/tm-core'); expect(version).toBe('1.0.0'); }); }); describe('utility functions', () => { it('should generate valid task IDs', () => { const id1 = generateTaskId(); const id2 = generateTaskId(); expect(typeof id1).toBe('string'); expect(typeof id2).toBe('string'); expect(id1).not.toBe(id2); // Should be unique expect(isValidTaskId(id1)).toBe(true); expect(isValidTaskId('')).toBe(false); }); it('should format dates', () => { const date = new Date('2023-01-01T00:00:00.000Z'); const formatted = formatDate(date); expect(formatted).toBe('2023-01-01T00:00:00.000Z'); }); }); describe('placeholder storage', () => { it('should perform basic storage operations', async () => { const storage = new PlaceholderStorage(); const testPath = 'test/path'; const testData = 'test data'; // Initially should not exist expect(await storage.exists(testPath)).toBe(false); expect(await storage.read(testPath)).toBe(null); // Write and verify await storage.write(testPath, testData); expect(await storage.exists(testPath)).toBe(true); expect(await storage.read(testPath)).toBe(testData); // Delete and verify await storage.delete(testPath); expect(await storage.exists(testPath)).toBe(false); }); }); describe('placeholder parser', () => { it('should parse simple task lists', async () => { const parser = new PlaceholderParser(); const content = ` - Task 1 - Task 2 - Task 3 `; const isValid = await parser.validate(content); expect(isValid).toBe(true); const tasks = await parser.parse(content); expect(tasks).toHaveLength(3); expect(tasks[0]?.title).toBe('Task 1'); expect(tasks[1]?.title).toBe('Task 2'); expect(tasks[2]?.title).toBe('Task 3'); tasks.forEach((task) => { expect(task.status).toBe('pending'); expect(task.priority).toBe('medium'); }); }); }); describe('error classes', () => { it('should create and throw custom errors', () => { const baseError = new TmCoreError('Base error'); expect(baseError.name).toBe('TmCoreError'); expect(baseError.message).toBe('Base error'); const taskNotFound = new TaskNotFoundError('task-123'); expect(taskNotFound.name).toBe('TaskNotFoundError'); expect(taskNotFound.code).toBe('TASK_NOT_FOUND'); expect(taskNotFound.message).toContain('task-123'); const validationError = new ValidationError('Invalid data'); expect(validationError.name).toBe('ValidationError'); expect(validationError.code).toBe('VALIDATION_ERROR'); const storageError = new StorageError('Storage failed'); expect(storageError.name).toBe('StorageError'); expect(storageError.code).toBe('STORAGE_ERROR'); }); }); describe('type definitions', () => { it('should have correct types available', () => { // These are compile-time checks that verify types exist const taskId: TaskId = 'test-id'; const status: TaskStatus = 'pending'; const priority: TaskPriority = 'high'; const task: PlaceholderTask = { id: taskId, title: 'Test Task', status: status, priority: priority }; expect(task.id).toBe('test-id'); expect(task.status).toBe('pending'); expect(task.priority).toBe('high'); }); }); }); ``` -------------------------------------------------------------------------------- /.taskmaster/docs/research/2025-06-14_test-the-fix-for-duplicate-saves-final-test.md: -------------------------------------------------------------------------------- ```markdown --- title: Research Session query: "test the fix for duplicate saves - final test" date: 6/14/2025 time: 6:10:00 PM timestamp: 2025-06-14T22:10:00.394Z exchanges: 1 --- # Research Session ## Initial Query **Question:** test the fix for duplicate saves - final test **Response:** ## Steps to Test the Fix for Duplicate Saves **1. Prepare a Clean Test Environment** - Ensure the tasks.json file and any related data stores are in a known, clean state with no pre-existing duplicate entries. - Back up the current tasks.json in case rollback is needed. **2. Define Test Scenarios** - Attempt to save a new task with unique data. - Attempt to save a task with the same ID or unique identifier as an existing task (simulate a duplicate). - Attempt to save a task with the same title or content but a different ID (to test if the fix is based on ID or content). - Simultaneously trigger multiple save operations (if the system supports concurrency) to test race conditions. **3. Execute Manual and Automated Tests** - Use the application's UI or API to create and save tasks as per the scenarios above. - For each save operation, check the tasks.json file or the data store to verify: - No duplicate entries are created. - Existing tasks are not overwritten unless intended (e.g., for updates). - The system returns appropriate error messages or warnings when a duplicate save is attempted. **4. Validate System Behavior** - Confirm that after all test operations, each task in tasks.json is unique according to the defined criteria (ID, title, or other unique fields). - If the system is designed to merge duplicates, verify that merging occurs as expected. - If the system is designed to reject duplicates, ensure that the rejection is clear and no duplicate is present. **5. Test Edge Cases** - Attempt to save tasks with minor variations (e.g., whitespace differences, case sensitivity) to ensure the duplicate detection logic is robust. - Test with large numbers of tasks to ensure performance and correctness at scale. **6. Review Logs and Error Handling** - Check application logs for any errors or warnings related to duplicate saves. - Ensure that error handling is user-friendly and provides actionable feedback. **7. Regression Testing** - Run the full suite of task operations (create, update, delete) to ensure the fix for duplicate saves does not introduce new issues elsewhere in the system. ## Additional Best Practices - Communicate test results with the team to confirm the fix is effective and to prevent future regressions[1][2][5]. - Consider implementing automated duplicate detection as part of the save operation to prevent future issues[2][3]. - Document the test cases and results for future reference and audits. ## Example Test Case Table | Test Scenario | Expected Result | Actual Result | Pass/Fail | |--------------------------------------|-------------------------------|---------------|-----------| | Save unique task | Task saved | | | | Save duplicate task (same ID) | Duplicate rejected/merged | | | | Save duplicate task (same title) | Duplicate rejected/merged | | | | Simultaneous saves (race condition) | Only one unique task present | | | | Save with minor variations | No false positives/negatives | | | Fill in the "Actual Result" and "Pass/Fail" columns during testing. ## Action Items - Complete all test scenarios above. - Document any issues found and retest after fixes. - Confirm with stakeholders before closing the issue. --- *Generated by Task Master Research Command* *Timestamp: 2025-06-14T22:10:00.394Z* ``` -------------------------------------------------------------------------------- /apps/docs/getting-started/quick-start/configuration-quick.mdx: -------------------------------------------------------------------------------- ```markdown --- title: Configuration sidebarTitle: "Configuration" --- Before getting started with Task Master, you'll need to set up your API keys. There are a couple of ways to do this depending on whether you're using the CLI or working inside MCP. It's also a good time to start getting familiar with the other configuration options available — even if you don’t need to adjust them yet, knowing what’s possible will help down the line. ## API Key Setup Task Master uses environment variables to securely store provider API keys and optional endpoint URLs. ### MCP Usage: mcp.json file For MCP/Cursor usage: Configure keys in the env section of your .cursor/mcp.json file. ```java .env lines icon="java" { "mcpServers": { "task-master-ai": { "command": "npx", "args": ["-y", "task-master-ai"], "env": { "ANTHROPIC_API_KEY": "ANTHROPIC_API_KEY_HERE", "PERPLEXITY_API_KEY": "PERPLEXITY_API_KEY_HERE", "OPENAI_API_KEY": "OPENAI_API_KEY_HERE", "GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE", "XAI_API_KEY": "XAI_API_KEY_HERE", "OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE", "MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE", "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE", "OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE", "GITHUB_API_KEY": "GITHUB_API_KEY_HERE" } } } } ``` ### CLI Usage: `.env` File Create a `.env` file in your project root and include the keys for the providers you plan to use: ```java .env lines icon="java" # Required API keys for providers configured in .taskmaster/config.json ANTHROPIC_API_KEY=sk-ant-api03-your-key-here PERPLEXITY_API_KEY=pplx-your-key-here # OPENAI_API_KEY=sk-your-key-here # GOOGLE_API_KEY=AIzaSy... # AZURE_OPENAI_API_KEY=your-azure-openai-api-key-here # etc. # Optional Endpoint Overrides # Use a specific provider's base URL, e.g., for an OpenAI-compatible API # OPENAI_BASE_URL=https://api.third-party.com/v1 # # Azure OpenAI Configuration # AZURE_OPENAI_ENDPOINT=https://your-resource-name.openai.azure.com/ or https://your-endpoint-name.cognitiveservices.azure.com/openai/deployments # OLLAMA_BASE_URL=http://custom-ollama-host:11434/api # Google Vertex AI Configuration (Required if using 'vertex' provider) # VERTEX_PROJECT_ID=your-gcp-project-id ``` ## What Else Can Be Configured? The main configuration file (`.taskmaster/config.json`) allows you to control nearly every aspect of Task Master’s behavior. Here’s a high-level look at what you can customize: <Tip> You don’t need to configure everything up front. Most settings can be left as defaults or updated later as your workflow evolves. </Tip> <Accordion title="View Configuration Options"> ### Models and Providers - Role-based model setup: `main`, `research`, `fallback` - Provider selection (Anthropic, OpenAI, Perplexity, etc.) - Model IDs per role - Temperature, max tokens, and other generation settings - Custom base URLs for OpenAI-compatible APIs ### Global Settings - `logLevel`: Logging verbosity - `debug`: Enable/disable debug mode - `projectName`: Optional name for your project - `defaultTag`: Default tag for task grouping - `defaultSubtasks`: Number of subtasks to auto-generate - `defaultPriority`: Priority level for new tasks ### API Endpoint Overrides - `ollamaBaseURL`: Custom Ollama server URL - `azureBaseURL`: Global Azure endpoint - `vertexProjectId`: Google Vertex AI project ID - `vertexLocation`: Region for Vertex AI models ### Tag and Git Integration - Default tag context per project - Support for task isolation by tag - Manual tag creation from Git branches ### State Management - Active tag tracking - Migration state - Last tag switch timestamp </Accordion> <Note> For advanced configuration options and detailed customization, see our [Advanced Configuration Guide](/docs/best-practices/configuration-advanced) page. </Note> ``` -------------------------------------------------------------------------------- /src/progress/tracker-ui.js: -------------------------------------------------------------------------------- ```javascript import chalk from 'chalk'; /** * Factory for creating progress bar elements */ class ProgressBarFactory { constructor(multibar) { if (!multibar) { throw new Error('Multibar instance is required'); } this.multibar = multibar; } /** * Creates a progress bar with the given format */ createBar(format, payload = {}) { if (typeof format !== 'string') { throw new Error('Format must be a string'); } const bar = this.multibar.create( 1, // total 1, // current {}, { format, barsize: 1, hideCursor: true, clearOnComplete: false } ); bar.update(1, payload); return bar; } /** * Creates a header with borders */ createHeader(headerFormat, borderFormat) { this.createBar(borderFormat); // Top border this.createBar(headerFormat); // Header this.createBar(borderFormat); // Bottom border } /** * Creates a data row */ createRow(rowFormat, payload) { if (!payload || typeof payload !== 'object') { throw new Error('Payload must be an object'); } return this.createBar(rowFormat, payload); } /** * Creates a border element */ createBorder(borderFormat) { return this.createBar(borderFormat); } } /** * Creates a bordered header for progress tables. * @param {Object} multibar - The multibar instance. * @param {string} headerFormat - Format string for the header row. * @param {string} borderFormat - Format string for the top and bottom borders. * @returns {void} */ export function createProgressHeader(multibar, headerFormat, borderFormat) { const factory = new ProgressBarFactory(multibar); factory.createHeader(headerFormat, borderFormat); } /** * Creates a formatted data row for progress tables. * @param {Object} multibar - The multibar instance. * @param {string} rowFormat - Format string for the row. * @param {Object} payload - Data payload for the row format. * @returns {void} */ export function createProgressRow(multibar, rowFormat, payload) { const factory = new ProgressBarFactory(multibar); factory.createRow(rowFormat, payload); } /** * Creates a border row for progress tables. * @param {Object} multibar - The multibar instance. * @param {string} borderFormat - Format string for the border. * @returns {void} */ export function createBorder(multibar, borderFormat) { const factory = new ProgressBarFactory(multibar); factory.createBorder(borderFormat); } /** * Builder for creating progress tables with consistent formatting */ export class ProgressTableBuilder { constructor(multibar) { this.factory = new ProgressBarFactory(multibar); this.borderStyle = '─'; this.columnSeparator = '|'; } /** * Shows a formatted table header */ showHeader(columns = null) { // Default columns for task display const defaultColumns = [ { text: 'TASK', width: 6 }, { text: 'PRI', width: 5 }, { text: 'TITLE', width: 64 } ]; const cols = columns || defaultColumns; const headerText = ' ' + cols.map((c) => c.text).join(' | ') + ' '; const borderLine = this.createBorderLine(cols.map((c) => c.width)); this.factory.createHeader(headerText, borderLine); return this; } /** * Creates a border line based on column widths */ createBorderLine(columnWidths) { return columnWidths .map((width) => this.borderStyle.repeat(width)) .join('─┼─'); } /** * Adds a task row to the table */ addTaskRow(taskId, priority, title) { const format = ` ${taskId} | ${priority} | {title}`; this.factory.createRow(format, { title }); // Add separator after each row const borderLine = '------+-----+' + '─'.repeat(64); this.factory.createBorder(borderLine); return this; } /** * Creates a summary row */ addSummaryRow(label, value) { const format = ` ${label}: {value}`; this.factory.createRow(format, { value }); return this; } } ``` -------------------------------------------------------------------------------- /tests/unit/mcp/tools/move-task-cross-tag-options.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // Mocks const mockFindTasksPath = jest .fn() .mockReturnValue('/test/path/.taskmaster/tasks/tasks.json'); jest.unstable_mockModule( '../../../../mcp-server/src/core/utils/path-utils.js', () => ({ findTasksPath: mockFindTasksPath }) ); const mockEnableSilentMode = jest.fn(); const mockDisableSilentMode = jest.fn(); jest.unstable_mockModule('../../../../scripts/modules/utils.js', () => ({ enableSilentMode: mockEnableSilentMode, disableSilentMode: mockDisableSilentMode })); // Spyable mock for moveTasksBetweenTags const mockMoveTasksBetweenTags = jest.fn(); jest.unstable_mockModule( '../../../../scripts/modules/task-manager/move-task.js', () => ({ moveTasksBetweenTags: mockMoveTasksBetweenTags }) ); // Import after mocks const { moveTaskCrossTagDirect } = await import( '../../../../mcp-server/src/core/direct-functions/move-task-cross-tag.js' ); describe('MCP Cross-Tag Move Direct Function - options & suggestions', () => { const mockLog = { info: jest.fn(), warn: jest.fn(), error: jest.fn() }; beforeEach(() => { jest.clearAllMocks(); }); it('passes only withDependencies/ignoreDependencies (no force) to core', async () => { // Arrange: make core throw tag validation after call to capture params mockMoveTasksBetweenTags.mockImplementation(() => { const err = new Error('Source tag "invalid" not found or invalid'); err.code = 'INVALID_SOURCE_TAG'; throw err; }); // Act await moveTaskCrossTagDirect( { sourceIds: '1,2', sourceTag: 'backlog', targetTag: 'in-progress', withDependencies: true, projectRoot: '/test' }, mockLog ); // Assert options argument (5th param) expect(mockMoveTasksBetweenTags).toHaveBeenCalled(); const args = mockMoveTasksBetweenTags.mock.calls[0]; const moveOptions = args[4]; expect(moveOptions).toEqual({ withDependencies: true, ignoreDependencies: false }); expect('force' in moveOptions).toBe(false); }); it('returns conflict suggestions on cross-tag dependency conflicts', async () => { // Arrange: core throws cross-tag dependency conflicts mockMoveTasksBetweenTags.mockImplementation(() => { const err = new Error( 'Cannot move tasks: 2 cross-tag dependency conflicts found' ); err.code = 'CROSS_TAG_DEPENDENCY_CONFLICTS'; throw err; }); // Act const result = await moveTaskCrossTagDirect( { sourceIds: '1', sourceTag: 'backlog', targetTag: 'in-progress', projectRoot: '/test' }, mockLog ); // Assert expect(result.success).toBe(false); expect(result.error.code).toBe('CROSS_TAG_DEPENDENCY_CONFLICT'); expect(Array.isArray(result.error.suggestions)).toBe(true); // Key suggestions const s = result.error.suggestions.join(' '); expect(s).toContain('--with-dependencies'); expect(s).toContain('--ignore-dependencies'); expect(s).toContain('validate-dependencies'); expect(s).toContain('Move dependencies first'); }); it('returns ID collision suggestions when target tag already has the ID', async () => { // Arrange: core throws TASK_ALREADY_EXISTS structured error mockMoveTasksBetweenTags.mockImplementation(() => { const err = new Error( 'Task 1 already exists in target tag "in-progress"' ); err.code = 'TASK_ALREADY_EXISTS'; throw err; }); // Act const result = await moveTaskCrossTagDirect( { sourceIds: '1', sourceTag: 'backlog', targetTag: 'in-progress', projectRoot: '/test' }, mockLog ); // Assert expect(result.success).toBe(false); expect(result.error.code).toBe('TASK_ALREADY_EXISTS'); const joined = (result.error.suggestions || []).join(' '); expect(joined).toContain('different target tag'); expect(joined).toContain('different set of IDs'); expect(joined).toContain('within-tag'); }); }); ``` -------------------------------------------------------------------------------- /.github/workflows/weekly-metrics-discord.yml: -------------------------------------------------------------------------------- ```yaml name: Weekly Metrics to Discord # description: Sends weekly metrics summary to Discord channel on: schedule: - cron: "0 9 * * 1" # Every Monday at 9 AM workflow_dispatch: permissions: contents: read issues: read pull-requests: read jobs: weekly-metrics: runs-on: ubuntu-latest env: DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Get dates for last 14 days run: | set -Eeuo pipefail # Last 14 days first_day=$(date -d "14 days ago" +%Y-%m-%d) last_day=$(date +%Y-%m-%d) echo "first_day=$first_day" >> $GITHUB_ENV echo "last_day=$last_day" >> $GITHUB_ENV echo "week_of=$(date -d '7 days ago' +'Week of %B %d, %Y')" >> $GITHUB_ENV echo "date_range=Past 14 days ($first_day to $last_day)" >> $GITHUB_ENV - name: Generate issue metrics uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: "repo:${{ github.repository }} is:issue created:${{ env.first_day }}..${{ env.last_day }}" HIDE_TIME_TO_ANSWER: true HIDE_LABEL_METRICS: false OUTPUT_FILE: issue_metrics.md - name: Generate PR created metrics uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: "repo:${{ github.repository }} is:pr created:${{ env.first_day }}..${{ env.last_day }}" OUTPUT_FILE: pr_created_metrics.md - name: Generate PR merged metrics uses: github/issue-metrics@v3 env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} SEARCH_QUERY: "repo:${{ github.repository }} is:pr is:merged merged:${{ env.first_day }}..${{ env.last_day }}" OUTPUT_FILE: pr_merged_metrics.md - name: Debug generated metrics run: | set -Eeuo pipefail echo "Listing markdown files in workspace:" ls -la *.md || true for f in issue_metrics.md pr_created_metrics.md pr_merged_metrics.md; do if [ -f "$f" ]; then echo "== $f (first 10 lines) ==" head -n 10 "$f" else echo "Missing $f" fi done - name: Parse metrics id: metrics run: node .github/scripts/parse-metrics.mjs - name: Send to Discord uses: sarisia/actions-status-discord@v1 if: env.DISCORD_WEBHOOK != '' with: webhook: ${{ env.DISCORD_WEBHOOK }} status: Success title: "📊 Weekly Metrics Report" description: | **${{ env.week_of }}** *${{ env.date_range }}* **🎯 Issues** • Created: ${{ steps.metrics.outputs.issues_created }} • Closed: ${{ steps.metrics.outputs.issues_closed }} • Avg Response Time: ${{ steps.metrics.outputs.issue_avg_first_response }} • Avg Time to Close: ${{ steps.metrics.outputs.issue_avg_time_to_close }} **🔀 Pull Requests** • Created: ${{ steps.metrics.outputs.prs_created }} • Merged: ${{ steps.metrics.outputs.prs_merged }} • Avg Response Time: ${{ steps.metrics.outputs.pr_avg_first_response }} • Avg Time to Merge: ${{ steps.metrics.outputs.pr_avg_merge_time }} **📈 Visual Analytics** https://repobeats.axiom.co/api/embed/b439f28f0ab5bd7a2da19505355693cd2c55bfd4.svg color: 0x58AFFF username: Task Master Metrics Bot avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png ``` -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/claude-code/message-converter.js: -------------------------------------------------------------------------------- ```javascript /** * @fileoverview Converts AI SDK prompt format to Claude Code message format */ /** * Convert AI SDK prompt to Claude Code messages format * @param {Array} prompt - AI SDK prompt array * @param {Object} [mode] - Generation mode * @param {string} mode.type - Mode type ('regular', 'object-json', 'object-tool') * @returns {{messagesPrompt: string, systemPrompt?: string}} */ export function convertToClaudeCodeMessages(prompt, mode) { const messages = []; let systemPrompt; for (const message of prompt) { switch (message.role) { case 'system': systemPrompt = message.content; break; case 'user': if (typeof message.content === 'string') { messages.push(message.content); } else { // Handle multi-part content const textParts = message.content .filter((part) => part.type === 'text') .map((part) => part.text) .join('\n'); if (textParts) { messages.push(textParts); } // Note: Image parts are not supported by Claude Code CLI const imageParts = message.content.filter( (part) => part.type === 'image' ); if (imageParts.length > 0) { console.warn( 'Claude Code CLI does not support image inputs. Images will be ignored.' ); } } break; case 'assistant': if (typeof message.content === 'string') { messages.push(`Assistant: ${message.content}`); } else { const textParts = message.content .filter((part) => part.type === 'text') .map((part) => part.text) .join('\n'); if (textParts) { messages.push(`Assistant: ${textParts}`); } // Handle tool calls if present const toolCalls = message.content.filter( (part) => part.type === 'tool-call' ); if (toolCalls.length > 0) { // For now, we'll just note that tool calls were made messages.push(`Assistant: [Tool calls made]`); } } break; case 'tool': // Tool results could be included in the conversation messages.push( `Tool Result (${message.content[0].toolName}): ${JSON.stringify( message.content[0].result )}` ); break; } } // For the SDK, we need to provide a single prompt string // Format the conversation history properly // Combine system prompt with messages let finalPrompt = ''; // Add system prompt at the beginning if present if (systemPrompt) { finalPrompt = systemPrompt; } if (messages.length === 0) { return { messagesPrompt: finalPrompt, systemPrompt }; } // Format messages const formattedMessages = []; for (let i = 0; i < messages.length; i++) { const msg = messages[i]; // Check if this is a user or assistant message based on content if (msg.startsWith('Assistant:') || msg.startsWith('Tool Result')) { formattedMessages.push(msg); } else { // User messages formattedMessages.push(`Human: ${msg}`); } } // Combine system prompt with messages if (finalPrompt) { finalPrompt = finalPrompt + '\n\n' + formattedMessages.join('\n\n'); } else { finalPrompt = formattedMessages.join('\n\n'); } // For JSON mode, add explicit instruction to ensure JSON output if (mode?.type === 'object-json') { // Make the JSON instruction even more explicit finalPrompt = `${finalPrompt} CRITICAL INSTRUCTION: You MUST respond with ONLY valid JSON. Follow these rules EXACTLY: 1. Start your response with an opening brace { 2. End your response with a closing brace } 3. Do NOT include any text before the opening brace 4. Do NOT include any text after the closing brace 5. Do NOT use markdown code blocks or backticks 6. Do NOT include explanations or commentary 7. The ENTIRE response must be valid JSON that can be parsed with JSON.parse() Begin your response with { and end with }`; } return { messagesPrompt: finalPrompt, systemPrompt }; } ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/utils/logger.ts: -------------------------------------------------------------------------------- ```typescript /** * Webview Logger Utility * Provides conditional logging based on environment */ type LogLevel = 'log' | 'warn' | 'error' | 'debug' | 'info'; interface LogEntry { level: LogLevel; message: string; data?: any; timestamp: number; } class WebviewLogger { private static instance: WebviewLogger; private enabled: boolean; private logHistory: LogEntry[] = []; private maxHistorySize = 100; private constructor() { // Enable logging in development, disable in production // Check for development mode via various indicators this.enabled = this.isDevelopment(); } static getInstance(): WebviewLogger { if (!WebviewLogger.instance) { WebviewLogger.instance = new WebviewLogger(); } return WebviewLogger.instance; } private isDevelopment(): boolean { // Check various indicators for development mode // VS Code webviews don't have process.env, so we check other indicators return ( // Check if running in localhost (development server) window.location.hostname === 'localhost' || // Check for development query parameter window.location.search.includes('debug=true') || // Check for VS Code development mode indicator (window as any).__VSCODE_DEV_MODE__ === true || // Default to false in production false ); } private addToHistory(entry: LogEntry): void { this.logHistory.push(entry); if (this.logHistory.length > this.maxHistorySize) { this.logHistory.shift(); } } private logMessage(level: LogLevel, message: string, ...args: any[]): void { const entry: LogEntry = { level, message, data: args.length > 0 ? args : undefined, timestamp: Date.now() }; this.addToHistory(entry); if (!this.enabled) { return; } // Format the message with timestamp const timestamp = new Date().toISOString(); const prefix = `[${timestamp}] [${level.toUpperCase()}]`; // Use appropriate console method switch (level) { case 'error': console.error(prefix, message, ...args); break; case 'warn': console.warn(prefix, message, ...args); break; case 'debug': console.debug(prefix, message, ...args); break; case 'info': console.info(prefix, message, ...args); break; default: console.log(prefix, message, ...args); } } log(message: string, ...args: any[]): void { this.logMessage('log', message, ...args); } error(message: string, ...args: any[]): void { // Always log errors, even in production const entry: LogEntry = { level: 'error', message, data: args.length > 0 ? args : undefined, timestamp: Date.now() }; this.addToHistory(entry); console.error(`[${new Date().toISOString()}] [ERROR]`, message, ...args); } warn(message: string, ...args: any[]): void { this.logMessage('warn', message, ...args); } debug(message: string, ...args: any[]): void { this.logMessage('debug', message, ...args); } info(message: string, ...args: any[]): void { this.logMessage('info', message, ...args); } // Enable/disable logging dynamically setEnabled(enabled: boolean): void { this.enabled = enabled; if (enabled) { console.log('[WebviewLogger] Logging enabled'); } } // Get log history (useful for debugging) getHistory(): LogEntry[] { return [...this.logHistory]; } // Clear log history clearHistory(): void { this.logHistory = []; } // Export logs as string (useful for bug reports) exportLogs(): string { return this.logHistory .map((entry) => { const timestamp = new Date(entry.timestamp).toISOString(); const data = entry.data ? JSON.stringify(entry.data) : ''; return `[${timestamp}] [${entry.level.toUpperCase()}] ${entry.message} ${data}`; }) .join('\n'); } } // Export singleton instance export const logger = WebviewLogger.getInstance(); // Export type for use in other files export type { WebviewLogger }; ``` -------------------------------------------------------------------------------- /src/profiles/roo.js: -------------------------------------------------------------------------------- ```javascript // Roo 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'; // Lifecycle functions for Roo 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', `[Roo] Source directory does not exist: ${sourceDir}`); return; } copyRecursiveSync(sourceDir, targetDir); log('debug', `[Roo] Copied roocode directory to ${targetDir}`); const rooModesDir = path.join(sourceDir, '.roo'); // Copy .roomodes to project root const roomodesSrc = path.join(sourceDir, '.roomodes'); const roomodesDest = path.join(targetDir, '.roomodes'); if (fs.existsSync(roomodesSrc)) { try { fs.copyFileSync(roomodesSrc, roomodesDest); log('debug', `[Roo] Copied .roomodes to ${roomodesDest}`); } catch (err) { log('error', `[Roo] Failed to copy .roomodes: ${err.message}`); } } for (const mode of ROO_MODES) { const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`); const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`); if (fs.existsSync(src)) { try { const destDir = path.dirname(dest); if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true }); fs.copyFileSync(src, dest); log('debug', `[Roo] Copied ${mode}-rules to ${dest}`); } catch (err) { log('error', `[Roo] Failed to copy ${src} to ${dest}: ${err.message}`); } } } } 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); } } function onRemoveRulesProfile(targetDir) { const roomodesPath = path.join(targetDir, '.roomodes'); if (fs.existsSync(roomodesPath)) { try { fs.rmSync(roomodesPath, { force: true }); log('debug', `[Roo] Removed .roomodes from ${roomodesPath}`); } catch (err) { log('error', `[Roo] Failed to remove .roomodes: ${err.message}`); } } const rooDir = path.join(targetDir, '.roo'); if (fs.existsSync(rooDir)) { fs.readdirSync(rooDir).forEach((entry) => { if (entry.startsWith('rules-')) { const modeDir = path.join(rooDir, entry); try { fs.rmSync(modeDir, { recursive: true, force: true }); log('debug', `[Roo] Removed ${entry} directory from ${modeDir}`); } catch (err) { log('error', `[Roo] Failed to remove ${modeDir}: ${err.message}`); } } }); if (fs.readdirSync(rooDir).length === 0) { try { fs.rmSync(rooDir, { recursive: true, force: true }); log('debug', `[Roo] Removed empty .roo directory from ${rooDir}`); } catch (err) { log('error', `[Roo] Failed to remove .roo directory: ${err.message}`); } } } } function onPostConvertRulesProfile(targetDir, assetsDir) { onAddRulesProfile(targetDir, assetsDir); } // Create and export roo profile using the base factory export const rooProfile = createProfile({ name: 'roo', displayName: 'Roo Code', url: 'roocode.com', docsUrl: 'docs.roocode.com', toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE, onAdd: onAddRulesProfile, onRemove: onRemoveRulesProfile, onPostConvert: onPostConvertRulesProfile }); // Export lifecycle functions separately to avoid naming conflicts export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile }; ```