This is page 8 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 -------------------------------------------------------------------------------- /tests/integration/profiles/roo-files-inclusion.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; import fs from 'fs'; import path from 'path'; import os from 'os'; import { execSync } from 'child_process'; describe('Roo Files Inclusion in Package', () => { // This test verifies that the required Roo files are included in the final package test('package.json includes dist/** in the "files" array for bundled files', () => { // Read the package.json file const packageJsonPath = path.join(process.cwd(), 'package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); // Check if dist/** is included in the files array (which contains bundled output including Roo files) expect(packageJson.files).toContain('dist/**'); }); test('roo.js profile contains logic for Roo directory creation and file copying', () => { // Read the roo.js profile file const rooJsPath = path.join(process.cwd(), 'src', 'profiles', 'roo.js'); const rooJsContent = fs.readFileSync(rooJsPath, 'utf8'); // Check for the main handler function expect( rooJsContent.includes('onAddRulesProfile(targetDir, assetsDir)') ).toBe(true); // Check for general recursive copy of assets/roocode expect( rooJsContent.includes('copyRecursiveSync(sourceDir, targetDir)') ).toBe(true); // Check for updated path handling expect(rooJsContent.includes("path.join(assetsDir, 'roocode')")).toBe(true); // Check for .roomodes file copying logic (source and destination paths) expect(rooJsContent.includes("path.join(sourceDir, '.roomodes')")).toBe( true ); expect(rooJsContent.includes("path.join(targetDir, '.roomodes')")).toBe( true ); // Check for mode-specific rule file copying logic expect(rooJsContent.includes('for (const mode of ROO_MODES)')).toBe(true); expect( rooJsContent.includes( 'path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`)' ) ).toBe(true); expect( rooJsContent.includes( "path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`)" ) ).toBe(true); // Check for import of ROO_MODES from profiles.js instead of local definition expect( rooJsContent.includes( "import { ROO_MODES } from '../constants/profiles.js'" ) ).toBe(true); // Verify ROO_MODES is used in the for loop expect(rooJsContent.includes('for (const mode of ROO_MODES)')).toBe(true); // Verify mode variable is used in the template strings (this confirms modes are being processed) expect(rooJsContent.includes('rules-${mode}')).toBe(true); expect(rooJsContent.includes('${mode}-rules')).toBe(true); // Verify that the ROO_MODES constant is properly imported and used // We should be able to find the template literals that use the mode variable expect(rooJsContent.includes('`rules-${mode}`')).toBe(true); expect(rooJsContent.includes('`${mode}-rules`')).toBe(true); expect(rooJsContent.includes('Copied ${mode}-rules to ${dest}')).toBe(true); // Also verify that the expected mode names are defined in the imported constant // by checking that the import is from the correct file that contains all 6 modes const profilesConstantsPath = path.join( process.cwd(), 'src', 'constants', 'profiles.js' ); const profilesContent = fs.readFileSync(profilesConstantsPath, 'utf8'); // Check that ROO_MODES is exported and contains all expected modes expect(profilesContent.includes('export const ROO_MODES')).toBe(true); const expectedModes = [ 'architect', 'ask', 'orchestrator', 'code', 'debug', 'test' ]; expectedModes.forEach((mode) => { expect(profilesContent.includes(`'${mode}'`)).toBe(true); }); }); test('source Roo files exist in assets directory', () => { // Verify that the source files for Roo integration exist expect( fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roo')) ).toBe(true); expect( fs.existsSync(path.join(process.cwd(), 'assets', 'roocode', '.roomodes')) ).toBe(true); }); }); ``` -------------------------------------------------------------------------------- /apps/extension/src/services/terminal-manager.ts: -------------------------------------------------------------------------------- ```typescript /** * Terminal Manager - Handles task execution in VS Code terminals * Uses @tm/core for consistent task management with the CLI */ import * as vscode from 'vscode'; import { createTaskMasterCore, type TaskMasterCore } from '@tm/core'; import type { ExtensionLogger } from '../utils/logger'; export interface TerminalExecutionOptions { taskId: string; taskTitle: string; tag?: string; } export interface TerminalExecutionResult { success: boolean; error?: string; terminalName?: string; } export class TerminalManager { private terminals = new Map<string, vscode.Terminal>(); private tmCore?: TaskMasterCore; constructor( private context: vscode.ExtensionContext, private logger: ExtensionLogger ) {} /** * Execute a task in a new VS Code terminal with Claude * Uses @tm/core for consistent task management with the CLI */ async executeTask( options: TerminalExecutionOptions ): Promise<TerminalExecutionResult> { const { taskTitle, tag } = options; // Ensure taskId is always a string const taskId = String(options.taskId); this.logger.log( `Starting task execution for ${taskId}: ${taskTitle}${tag ? ` (tag: ${tag})` : ''}` ); this.logger.log(`TaskId type: ${typeof taskId}, value: ${taskId}`); try { // Initialize tm-core if needed await this.initializeCore(); // Use tm-core to start the task (same as CLI) const startResult = await this.tmCore!.startTask(taskId, { dryRun: false, force: false, updateStatus: true }); if (!startResult.started || !startResult.executionOutput) { throw new Error( startResult.error || 'Failed to start task with tm-core' ); } // Create terminal with custom TaskMaster icon const terminalName = `Task ${taskId}: ${taskTitle}`; const terminal = this.createTerminal(terminalName); // Store terminal reference for potential cleanup this.terminals.set(taskId, terminal); // Show terminal and run Claude command terminal.show(); const command = `claude "${startResult.executionOutput}"`; terminal.sendText(command); this.logger.log(`Launched Claude for task ${taskId} using tm-core`); return { success: true, terminalName }; } catch (error) { this.logger.error('Failed to execute task:', error); return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } } /** * Create a new terminal with TaskMaster branding */ private createTerminal(name: string): vscode.Terminal { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; return vscode.window.createTerminal({ name, cwd: workspaceRoot, iconPath: new vscode.ThemeIcon('play') // Use a VS Code built-in icon for now }); } /** * Initialize TaskMaster Core (same as CLI) */ private async initializeCore(): Promise<void> { if (!this.tmCore) { const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!workspaceRoot) { throw new Error('No workspace folder found'); } this.tmCore = await createTaskMasterCore({ projectPath: workspaceRoot }); } } /** * Get terminal by task ID (if still active) */ getTerminalByTaskId(taskId: string): vscode.Terminal | undefined { return this.terminals.get(taskId); } /** * Clean up terminated terminals */ cleanupTerminal(taskId: string): void { const terminal = this.terminals.get(taskId); if (terminal) { this.terminals.delete(taskId); } } /** * Dispose all managed terminals and clean up tm-core */ async dispose(): Promise<void> { this.terminals.forEach((terminal) => { try { terminal.dispose(); } catch (error) { this.logger.error('Failed to dispose terminal:', error); } }); this.terminals.clear(); if (this.tmCore) { try { await this.tmCore.close(); this.tmCore = undefined; } catch (error) { this.logger.error('Failed to close tm-core:', error); } } } } ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/remove-task.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; // --- Mock dependencies BEFORE module import --- jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ readJSON: jest.fn(), writeJSON: jest.fn(), log: jest.fn(), CONFIG: { model: 'mock-model', maxTokens: 4000, temperature: 0.7, debug: false }, findTaskById: jest.fn(), truncate: jest.fn((t) => t), isSilentMode: jest.fn(() => false) })); jest.unstable_mockModule( '../../../../../scripts/modules/task-manager/generate-task-files.js', () => ({ default: jest.fn().mockResolvedValue() }) ); // fs is used for file deletion side-effects – stub the methods we touch jest.unstable_mockModule('fs', () => ({ existsSync: jest.fn(() => true), unlinkSync: jest.fn() })); // path is fine to keep as real since only join/dirname used – no side effects // Import mocked modules const { readJSON, writeJSON, log } = await import( '../../../../../scripts/modules/utils.js' ); const generateTaskFiles = ( await import( '../../../../../scripts/modules/task-manager/generate-task-files.js' ) ).default; const fs = await import('fs'); // Import module under test (AFTER mocks in place) const { default: removeTask } = await import( '../../../../../scripts/modules/task-manager/remove-task.js' ); // ---- Test data helpers ---- const buildSampleTaggedTasks = () => ({ master: { tasks: [ { id: 1, title: 'Task 1', status: 'pending', dependencies: [] }, { id: 2, title: 'Task 2', status: 'pending', dependencies: [1] }, { id: 3, title: 'Parent', status: 'pending', dependencies: [], subtasks: [ { id: 1, title: 'Sub 3.1', status: 'pending', dependencies: [] } ] } ] }, other: { tasks: [{ id: 99, title: 'Shadow', status: 'pending', dependencies: [1] }] } }); // Utility to deep clone sample each test const getFreshData = () => JSON.parse(JSON.stringify(buildSampleTaggedTasks())); // ----- Tests ----- describe('removeTask', () => { beforeEach(() => { jest.clearAllMocks(); // readJSON returns deep copy so each test isolated readJSON.mockImplementation(() => { return { ...getFreshData().master, tag: 'master', _rawTaggedData: getFreshData() }; }); writeJSON.mockResolvedValue(); log.mockImplementation(() => {}); fs.unlinkSync.mockImplementation(() => {}); }); test('removes a main task and cleans dependencies across tags', async () => { const result = await removeTask('tasks/tasks.json', '1', { tag: 'master' }); // Expect success true expect(result.success).toBe(true); // writeJSON called with data where task 1 is gone in master & dependencies removed in other tags const written = writeJSON.mock.calls[0][1]; expect(written.master.tasks.find((t) => t.id === 1)).toBeUndefined(); // deps removed from child tasks const task2 = written.master.tasks.find((t) => t.id === 2); expect(task2.dependencies).not.toContain(1); const shadow = written.other.tasks.find((t) => t.id === 99); expect(shadow.dependencies).not.toContain(1); // Task file deletion attempted expect(fs.unlinkSync).toHaveBeenCalled(); }); test('removes a subtask only and leaves parent intact', async () => { const result = await removeTask('tasks/tasks.json', '3.1', { tag: 'master' }); expect(result.success).toBe(true); const written = writeJSON.mock.calls[0][1]; const parent = written.master.tasks.find((t) => t.id === 3); expect(parent.subtasks || []).toHaveLength(0); // Ensure parent still exists expect(parent).toBeDefined(); // No task files should be deleted for subtasks expect(fs.unlinkSync).not.toHaveBeenCalled(); }); test('handles non-existent task gracefully', async () => { const result = await removeTask('tasks/tasks.json', '42', { tag: 'master' }); expect(result.success).toBe(false); expect(result.error).toContain('not found'); // writeJSON not called because nothing changed expect(writeJSON).not.toHaveBeenCalled(); }); }); ``` -------------------------------------------------------------------------------- /tests/unit/prompts/expand-task-prompt.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; import { PromptManager } from '../../../scripts/modules/prompt-manager.js'; describe('expand-task prompt template', () => { let promptManager; beforeEach(() => { promptManager = new PromptManager(); }); const testTask = { id: 1, title: 'Setup AWS Infrastructure', description: 'Provision core AWS services', details: 'Create VPC, subnets, and security groups' }; const baseParams = { task: testTask, subtaskCount: 3, nextSubtaskId: 1, additionalContext: '', complexityReasoningContext: '', gatheredContext: '', useResearch: false, expansionPrompt: undefined }; test('default variant includes task context', () => { const { userPrompt } = promptManager.loadPrompt( 'expand-task', baseParams, 'default' ); expect(userPrompt).toContain(testTask.title); expect(userPrompt).toContain(testTask.description); expect(userPrompt).toContain(testTask.details); expect(userPrompt).toContain('Task ID: 1'); }); test('research variant includes task context', () => { const params = { ...baseParams, useResearch: true }; const { userPrompt } = promptManager.loadPrompt( 'expand-task', params, 'research' ); expect(userPrompt).toContain(testTask.title); expect(userPrompt).toContain(testTask.description); expect(userPrompt).toContain(testTask.details); expect(userPrompt).toContain('Parent Task:'); expect(userPrompt).toContain('ID: 1'); }); test('complexity-report variant includes task context', () => { const params = { ...baseParams, expansionPrompt: 'Focus on security best practices', complexityReasoningContext: 'High complexity due to security requirements' }; const { userPrompt } = promptManager.loadPrompt( 'expand-task', params, 'complexity-report' ); // The fix ensures task context is included expect(userPrompt).toContain('Parent Task:'); expect(userPrompt).toContain(`ID: ${testTask.id}`); expect(userPrompt).toContain(`Title: ${testTask.title}`); expect(userPrompt).toContain(`Description: ${testTask.description}`); expect(userPrompt).toContain(`Current details: ${testTask.details}`); // Also includes the expansion prompt expect(userPrompt).toContain('Expansion Guidance:'); expect(userPrompt).toContain(params.expansionPrompt); expect(userPrompt).toContain(params.complexityReasoningContext); }); test('all variants request JSON format with subtasks array', () => { const variants = ['default', 'research', 'complexity-report']; variants.forEach((variant) => { const params = variant === 'complexity-report' ? { ...baseParams, expansionPrompt: 'test' } : baseParams; const { systemPrompt, userPrompt } = promptManager.loadPrompt( 'expand-task', params, variant ); const combined = systemPrompt + userPrompt; expect(combined.toLowerCase()).toContain('subtasks'); expect(combined).toContain('JSON'); }); }); test('complexity-report variant fails without task context regression test', () => { // This test ensures we don't regress to the old behavior where // complexity-report variant only used expansionPrompt without task context const params = { ...baseParams, expansionPrompt: 'Generic expansion prompt' }; const { userPrompt } = promptManager.loadPrompt( 'expand-task', params, 'complexity-report' ); // Count occurrences of task-specific content const titleOccurrences = ( userPrompt.match(new RegExp(testTask.title, 'g')) || [] ).length; const descriptionOccurrences = ( userPrompt.match(new RegExp(testTask.description, 'g')) || [] ).length; // Should have at least one occurrence of title and description expect(titleOccurrences).toBeGreaterThanOrEqual(1); expect(descriptionOccurrences).toBeGreaterThanOrEqual(1); // Should not be ONLY the expansion prompt expect(userPrompt.length).toBeGreaterThan( params.expansionPrompt.length + 100 ); }); }); ``` -------------------------------------------------------------------------------- /src/ai-providers/custom-sdk/grok-cli/errors.js: -------------------------------------------------------------------------------- ```javascript /** * @fileoverview Error handling utilities for Grok CLI provider */ import { APICallError, LoadAPIKeyError } from '@ai-sdk/provider'; /** * @typedef {import('./types.js').GrokCliErrorMetadata} GrokCliErrorMetadata */ /** * Create an API call error with Grok CLI specific metadata * @param {Object} params - Error parameters * @param {string} params.message - Error message * @param {string} [params.code] - Error code * @param {number} [params.exitCode] - Process exit code * @param {string} [params.stderr] - Standard error output * @param {string} [params.stdout] - Standard output * @param {string} [params.promptExcerpt] - Excerpt of the prompt * @param {boolean} [params.isRetryable=false] - Whether the error is retryable * @returns {APICallError} */ export function createAPICallError({ message, code, exitCode, stderr, stdout, promptExcerpt, isRetryable = false }) { /** @type {GrokCliErrorMetadata} */ const metadata = { code, exitCode, stderr, stdout, promptExcerpt }; return new APICallError({ message, isRetryable, url: 'grok-cli://command', requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined, data: metadata }); } /** * Create an authentication error * @param {Object} params - Error parameters * @param {string} params.message - Error message * @returns {LoadAPIKeyError} */ export function createAuthenticationError({ message }) { return new LoadAPIKeyError({ message: message || 'Authentication failed. Please ensure Grok CLI is properly configured with API key.' }); } /** * Create a timeout error * @param {Object} params - Error parameters * @param {string} params.message - Error message * @param {string} [params.promptExcerpt] - Excerpt of the prompt * @param {number} params.timeoutMs - Timeout in milliseconds * @returns {APICallError} */ export function createTimeoutError({ message, promptExcerpt, timeoutMs }) { /** @type {GrokCliErrorMetadata & { timeoutMs: number }} */ const metadata = { code: 'TIMEOUT', promptExcerpt, timeoutMs }; return new APICallError({ message, isRetryable: true, url: 'grok-cli://command', requestBodyValues: promptExcerpt ? { prompt: promptExcerpt } : undefined, data: metadata }); } /** * Create a CLI installation error * @param {Object} params - Error parameters * @param {string} [params.message] - Error message * @returns {APICallError} */ export function createInstallationError({ message }) { return new APICallError({ message: message || 'Grok CLI is not installed or not found in PATH. Please install with: npm install -g @vibe-kit/grok-cli', isRetryable: false, url: 'grok-cli://installation' }); } /** * Check if an error is an authentication error * @param {unknown} error - Error to check * @returns {boolean} */ export function isAuthenticationError(error) { if (error instanceof LoadAPIKeyError) return true; if ( error instanceof APICallError && /** @type {GrokCliErrorMetadata} */ (error.data)?.exitCode === 401 ) return true; return false; } /** * Check if an error is a timeout error * @param {unknown} error - Error to check * @returns {boolean} */ export function isTimeoutError(error) { if ( error instanceof APICallError && /** @type {GrokCliErrorMetadata} */ (error.data)?.code === 'TIMEOUT' ) return true; return false; } /** * Check if an error is an installation error * @param {unknown} error - Error to check * @returns {boolean} */ export function isInstallationError(error) { if (error instanceof APICallError && error.url === 'grok-cli://installation') return true; return false; } /** * Get error metadata from an error * @param {unknown} error - Error to extract metadata from * @returns {GrokCliErrorMetadata|undefined} */ export function getErrorMetadata(error) { if (error instanceof APICallError && error.data) { return /** @type {GrokCliErrorMetadata} */ (error.data); } return undefined; } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/config/services/config-loader.service.spec.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview Unit tests for ConfigLoader service */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { promises as fs } from 'node:fs'; import { ConfigLoader } from './config-loader.service.js'; import { DEFAULT_CONFIG_VALUES } from '../../interfaces/configuration.interface.js'; vi.mock('node:fs', () => ({ promises: { readFile: vi.fn(), access: vi.fn() } })); describe('ConfigLoader', () => { let configLoader: ConfigLoader; const testProjectRoot = '/test/project'; beforeEach(() => { configLoader = new ConfigLoader(testProjectRoot); vi.clearAllMocks(); }); afterEach(() => { vi.restoreAllMocks(); }); describe('getDefaultConfig', () => { it('should return default configuration values', () => { const config = configLoader.getDefaultConfig(); expect(config.models).toEqual({ main: DEFAULT_CONFIG_VALUES.MODELS.MAIN, fallback: DEFAULT_CONFIG_VALUES.MODELS.FALLBACK }); expect(config.storage).toEqual({ type: DEFAULT_CONFIG_VALUES.STORAGE.TYPE, encoding: DEFAULT_CONFIG_VALUES.STORAGE.ENCODING, enableBackup: false, maxBackups: DEFAULT_CONFIG_VALUES.STORAGE.MAX_BACKUPS, enableCompression: false, atomicOperations: true }); expect(config.version).toBe(DEFAULT_CONFIG_VALUES.VERSION); }); }); describe('loadLocalConfig', () => { it('should load and parse local configuration file', async () => { const mockConfig = { models: { main: 'test-model' }, storage: { type: 'api' as const } }; vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockConfig)); const result = await configLoader.loadLocalConfig(); expect(fs.readFile).toHaveBeenCalledWith( '/test/project/.taskmaster/config.json', 'utf-8' ); expect(result).toEqual(mockConfig); }); it('should return null when config file does not exist', async () => { const error = new Error('File not found') as any; error.code = 'ENOENT'; vi.mocked(fs.readFile).mockRejectedValue(error); const result = await configLoader.loadLocalConfig(); expect(result).toBeNull(); }); it('should throw TaskMasterError for other file errors', async () => { const error = new Error('Permission denied'); vi.mocked(fs.readFile).mockRejectedValue(error); await expect(configLoader.loadLocalConfig()).rejects.toThrow( 'Failed to load local configuration' ); }); it('should throw error for invalid JSON', async () => { vi.mocked(fs.readFile).mockResolvedValue('invalid json'); await expect(configLoader.loadLocalConfig()).rejects.toThrow(); }); }); describe('loadGlobalConfig', () => { it('should return null (not implemented yet)', async () => { const result = await configLoader.loadGlobalConfig(); expect(result).toBeNull(); }); }); describe('hasLocalConfig', () => { it('should return true when local config exists', async () => { vi.mocked(fs.access).mockResolvedValue(undefined); const result = await configLoader.hasLocalConfig(); expect(fs.access).toHaveBeenCalledWith( '/test/project/.taskmaster/config.json' ); expect(result).toBe(true); }); it('should return false when local config does not exist', async () => { vi.mocked(fs.access).mockRejectedValue(new Error('Not found')); const result = await configLoader.hasLocalConfig(); expect(result).toBe(false); }); }); describe('hasGlobalConfig', () => { it('should check global config path', async () => { vi.mocked(fs.access).mockResolvedValue(undefined); const result = await configLoader.hasGlobalConfig(); expect(fs.access).toHaveBeenCalledWith( expect.stringContaining('.taskmaster/config.json') ); expect(result).toBe(true); }); it('should return false when global config does not exist', async () => { vi.mocked(fs.access).mockRejectedValue(new Error('Not found')); const result = await configLoader.hasGlobalConfig(); expect(result).toBe(false); }); }); }); ``` -------------------------------------------------------------------------------- /apps/extension/src/components/ui/shadcn-io/kanban/index.tsx: -------------------------------------------------------------------------------- ```typescript 'use client'; import { Card } from '@/components/ui/card'; import { cn } from '@/lib/utils'; import { DndContext, DragOverlay, MouseSensor, TouchSensor, rectIntersection, useDraggable, useDroppable, useSensor, useSensors } from '@dnd-kit/core'; import type { DragEndEvent } from '@dnd-kit/core'; import type React from 'react'; import type { ReactNode } from 'react'; export type { DragEndEvent } from '@dnd-kit/core'; export type Status = { id: string; name: string; color: string; }; export type Feature = { id: string; name: string; startAt: Date; endAt: Date; status: Status; }; export type KanbanBoardProps = { id: Status['id']; children: ReactNode; className?: string; }; export const KanbanBoard = ({ id, children, className }: KanbanBoardProps) => { const { isOver, setNodeRef } = useDroppable({ id }); return ( <div className={cn( 'flex h-full min-h-40 flex-col gap-2 rounded-md border bg-secondary p-2 text-xs shadow-sm outline transition-all', isOver ? 'outline-primary' : 'outline-transparent', className )} ref={setNodeRef} > {children} </div> ); }; export type KanbanCardProps = Pick<Feature, 'id' | 'name'> & { index: number; parent: string; children?: ReactNode; className?: string; onClick?: (event: React.MouseEvent) => void; onDoubleClick?: (event: React.MouseEvent) => void; }; export const KanbanCard = ({ id, name, index, parent, children, className, onClick, onDoubleClick }: KanbanCardProps) => { const { attributes, listeners, setNodeRef, transform, isDragging } = useDraggable({ id, data: { index, parent } }); return ( <Card className={cn( 'rounded-md p-3 shadow-sm', isDragging && 'cursor-grabbing opacity-0', !isDragging && 'cursor-pointer', className )} style={{ transform: transform ? `translateX(${transform.x}px) translateY(${transform.y}px)` : 'none' }} {...attributes} {...listeners} onClick={(e) => !isDragging && onClick?.(e)} onDoubleClick={onDoubleClick} ref={setNodeRef} > {children ?? <p className="m-0 font-medium text-sm">{name}</p>} </Card> ); }; export type KanbanCardsProps = { children: ReactNode; className?: string; }; export const KanbanCards = ({ children, className }: KanbanCardsProps) => ( <div className={cn('flex flex-1 flex-col gap-2', className)}>{children}</div> ); export type KanbanHeaderProps = | { children: ReactNode; } | { name: Status['name']; color: Status['color']; className?: string; }; export const KanbanHeader = (props: KanbanHeaderProps) => 'children' in props ? ( props.children ) : ( <div className={cn('flex shrink-0 items-center gap-2', props.className)}> <div className="h-2 w-2 rounded-full" style={{ backgroundColor: props.color }} /> <p className="m-0 font-semibold text-sm">{props.name}</p> </div> ); export type KanbanProviderProps = { children: ReactNode; onDragEnd: (event: DragEndEvent) => void; onDragStart?: (event: DragEndEvent) => void; onDragCancel?: () => void; className?: string; dragOverlay?: ReactNode; }; export const KanbanProvider = ({ children, onDragEnd, onDragStart, onDragCancel, className, dragOverlay }: KanbanProviderProps) => { // Configure sensors with activation constraints to prevent accidental drags const sensors = useSensors( // Only start a drag if you've moved more than 8px useSensor(MouseSensor, { activationConstraint: { distance: 8 } }), // On touch devices, require a short press + small move useSensor(TouchSensor, { activationConstraint: { delay: 150, tolerance: 5 } }) ); return ( <DndContext sensors={sensors} collisionDetection={rectIntersection} onDragEnd={onDragEnd} onDragStart={onDragStart} onDragCancel={onDragCancel} > <div className={cn( 'grid w-full auto-cols-fr grid-flow-col gap-4', className )} > {children} </div> <DragOverlay>{dragOverlay}</DragOverlay> </DndContext> ); }; ``` -------------------------------------------------------------------------------- /.taskmaster/docs/research/2025-06-14_test-save-functionality.md: -------------------------------------------------------------------------------- ```markdown --- title: Research Session query: "test save functionality" date: 6/14/2025 time: 5:50:23 PM timestamp: 2025-06-14T21:50:23.797Z exchanges: 1 --- # Research Session ## Initial Query **Question:** test save functionality **Response:** ## Testing Save Functionality in a Task Management System **Core Steps for Testing Save Functionality** - Create or update a task in the system using the CLI or UI. - Trigger the save operation (e.g., by issuing a command or clicking a save button). - Verify that the data is correctly written to the persistent storage (e.g., `tasks.json`). - Reload or re-query the data to ensure the saved changes persist and are accurately reflected. - Validate that all required fields and relationships (such as dependencies and subtasks) are preserved and correctly structured. **Key Test Scenarios** - **Basic Save:** Add a new task and confirm it appears in `tasks.json` with all fields populated. - **Update Save:** Modify an existing task (e.g., change status, description, or priority) and verify the changes are saved. - **Subtask and Dependency Preservation:** Update a parent task and ensure that completed subtasks and dependency links remain intact, especially after using commands like `updateTask`. - **Batch Operations:** Perform bulk updates or additions and confirm all affected tasks are saved correctly, with no data loss or corruption. - **Validation:** Attempt to save invalid data and ensure the system rejects it or provides meaningful error messages, confirming validation logic is enforced. **Recommended Practices** - Use automated tests to simulate save operations for various scenarios, including edge cases (e.g., circular dependencies, missing required fields). - After each save, reload the data and compare it to the expected state to ensure integrity. - Log all save operations and errors for traceability and debugging. - Regularly back up the `tasks.json` file to prevent data loss during testing and development. **Relevant Features from Industry Standards** - Task management systems typically provide robust save and update mechanisms, ensuring that all task attributes (including dependencies, subtasks, and comments) are consistently maintained[1][2][5]. - Validation and error handling are critical to prevent corrupt or incomplete saves, especially when handling complex relationships or batch operations[1][5]. - User feedback (such as success messages or error alerts) is important for confirming save actions and guiding users in case of issues[1][2]. **Example Test Case** 1. Create a task with subtasks and dependencies. 2. Mark some subtasks as complete. 3. Use the `updateTask` command to modify the parent task. 4. Save and reload the data. 5. Assert that: - The parent task reflects the updates. - Completed subtasks remain marked as complete. - Dependencies are unchanged and valid. **Summary Table: Save Functionality Test Aspects** | Aspect | What to Test | Expected Outcome | |-----------------------|---------------------------------------------------|-----------------------------------------| | Basic Save | Add/update task, save, reload | Data persists and matches expectations | | Subtask Preservation | Update parent, check subtask status | Completed subtasks remain complete | | Dependency Handling | Save with dependencies, reload | Dependencies are intact and valid | | Batch Operations | Bulk save/update, reload | All changes persist, no data loss | | Validation | Save invalid data | Error is raised, data not corrupted | These steps and scenarios ensure that the save functionality is robust, reliable, and meets the requirements of a modern task management system[1][2][5]. --- *Generated by Task Master Research Command* *Timestamp: 2025-06-14T21:50:23.797Z* ``` -------------------------------------------------------------------------------- /packages/tm-core/src/utils/id-generator.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview ID generation utilities for Task Master * Provides functions to generate unique identifiers for tasks and subtasks */ import { randomBytes } from 'node:crypto'; /** * Generates a unique task ID using the format: TASK-{timestamp}-{random} * * @returns A unique task ID string * @example * ```typescript * const taskId = generateTaskId(); * // Returns something like: "TASK-1704067200000-A7B3" * ``` */ export function generateTaskId(): string { const timestamp = Date.now(); const random = generateRandomString(4); return `TASK-${timestamp}-${random}`; } /** * Generates a subtask ID using the format: {parentId}.{sequential} * * @param parentId - The ID of the parent task * @param existingSubtasks - Array of existing subtask IDs to determine the next sequential number * @returns A unique subtask ID string * @example * ```typescript * const subtaskId = generateSubtaskId("TASK-123-A7B3", ["TASK-123-A7B3.1"]); * // Returns: "TASK-123-A7B3.2" * ``` */ export function generateSubtaskId( parentId: string, existingSubtasks: string[] = [] ): string { // Find existing subtasks for this parent const parentSubtasks = existingSubtasks.filter((id) => id.startsWith(`${parentId}.`) ); // Extract sequential numbers and find the highest const sequentialNumbers = parentSubtasks .map((id) => { const parts = id.split('.'); const lastPart = parts[parts.length - 1]; return Number.parseInt(lastPart, 10); }) .filter((num) => !Number.isNaN(num)) .sort((a, b) => a - b); // Determine the next sequential number const nextSequential = sequentialNumbers.length > 0 ? Math.max(...sequentialNumbers) + 1 : 1; return `${parentId}.${nextSequential}`; } /** * Generates a random alphanumeric string of specified length * Uses crypto.randomBytes for cryptographically secure randomness * * @param length - The desired length of the random string * @returns A random alphanumeric string * @internal */ function generateRandomString(length: number): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const bytes = randomBytes(length); let result = ''; for (let i = 0; i < length; i++) { result += chars[bytes[i] % chars.length]; } return result; } /** * Validates a task ID format * * @param id - The ID to validate * @returns True if the ID matches the expected task ID format * @example * ```typescript * isValidTaskId("TASK-1704067200000-A7B3"); // true * isValidTaskId("invalid-id"); // false * ``` */ export function isValidTaskId(id: string): boolean { const taskIdRegex = /^TASK-\d{13}-[A-Z0-9]{4}$/; return taskIdRegex.test(id); } /** * Validates a subtask ID format * * @param id - The ID to validate * @returns True if the ID matches the expected subtask ID format * @example * ```typescript * isValidSubtaskId("TASK-1704067200000-A7B3.1"); // true * isValidSubtaskId("TASK-1704067200000-A7B3.1.2"); // true (nested subtask) * isValidSubtaskId("invalid.id"); // false * ``` */ export function isValidSubtaskId(id: string): boolean { const parts = id.split('.'); if (parts.length < 2) return false; // First part should be a valid task ID const taskIdPart = parts[0]; if (!isValidTaskId(taskIdPart)) return false; // Remaining parts should be positive integers const sequentialParts = parts.slice(1); return sequentialParts.every((part) => { const num = Number.parseInt(part, 10); return !Number.isNaN(num) && num > 0 && part === num.toString(); }); } /** * Extracts the parent task ID from a subtask ID * * @param subtaskId - The subtask ID * @returns The parent task ID, or null if the input is not a valid subtask ID * @example * ```typescript * getParentTaskId("TASK-1704067200000-A7B3.1.2"); // "TASK-1704067200000-A7B3" * getParentTaskId("TASK-1704067200000-A7B3"); // null (not a subtask) * ``` */ export function getParentTaskId(subtaskId: string): string | null { if (!isValidSubtaskId(subtaskId)) return null; const parts = subtaskId.split('.'); return parts[0]; } ``` -------------------------------------------------------------------------------- /apps/extension/src/services/task-repository.ts: -------------------------------------------------------------------------------- ```typescript /** * Task Repository - Simplified version * Handles data access with caching */ import { EventEmitter } from '../utils/event-emitter'; import type { ExtensionLogger } from '../utils/logger'; import type { TaskMasterApi, TaskMasterTask } from '../utils/task-master-api'; // Use the TaskMasterTask type directly to ensure compatibility export type Task = TaskMasterTask; export class TaskRepository extends EventEmitter { private cache: Task[] | null = null; private cacheTimestamp = 0; private readonly CACHE_DURATION = 30000; // 30 seconds constructor( private api: TaskMasterApi, private logger: ExtensionLogger ) { super(); } async getAll(options?: { tag?: string; withSubtasks?: boolean; }): Promise<Task[]> { // If a tag is specified, always fetch fresh data const shouldUseCache = !options?.tag && this.cache && Date.now() - this.cacheTimestamp < this.CACHE_DURATION; if (shouldUseCache) { return this.cache || []; } try { const result = await this.api.getTasks({ withSubtasks: options?.withSubtasks ?? true, tag: options?.tag }); if (result.success && result.data) { this.cache = result.data; this.cacheTimestamp = Date.now(); this.emit('tasks:updated', result.data); return result.data; } throw new Error(result.error || 'Failed to fetch tasks'); } catch (error) { this.logger.error('Failed to get tasks', error); throw error; } } async getById(taskId: string): Promise<Task | null> { // First check cache if (this.cache) { // Handle both main tasks and subtasks for (const task of this.cache) { if (task.id === taskId) { return task; } // Check subtasks if (task.subtasks) { for (const subtask of task.subtasks) { if ( subtask.id.toString() === taskId || `${task.id}.${subtask.id}` === taskId ) { return { ...subtask, id: subtask.id.toString(), description: subtask.description || '', status: (subtask.status || 'pending') as TaskMasterTask['status'], priority: 'medium' as const, dependencies: subtask.dependencies?.map((d) => d.toString()) || [] }; } } } } } // If not in cache, fetch all and search const tasks = await this.getAll(); for (const task of tasks) { if (task.id === taskId) { return task; } // Check subtasks if (task.subtasks) { for (const subtask of task.subtasks) { if ( subtask.id.toString() === taskId || `${task.id}.${subtask.id}` === taskId ) { return { ...subtask, id: subtask.id.toString(), description: subtask.description || '', status: (subtask.status || 'pending') as TaskMasterTask['status'], priority: 'medium' as const, dependencies: subtask.dependencies?.map((d) => d.toString()) || [] }; } } } } return null; } async updateStatus(taskId: string, status: Task['status']): Promise<void> { try { const result = await this.api.updateTaskStatus(taskId, status); if (!result.success) { throw new Error(result.error || 'Failed to update status'); } // Invalidate cache this.cache = null; // Fetch updated tasks await this.getAll(); } catch (error) { this.logger.error('Failed to update task status', error); throw error; } } async updateContent(taskId: string, updates: any): Promise<void> { try { const result = await this.api.updateTask(taskId, updates, { append: false, research: false }); if (!result.success) { throw new Error(result.error || 'Failed to update task'); } // Invalidate cache this.cache = null; // Fetch updated tasks await this.getAll(); } catch (error) { this.logger.error('Failed to update task content', error); throw error; } } async refresh(): Promise<void> { this.cache = null; await this.getAll(); } isConnected(): boolean { return this.api.getConnectionStatus().isConnected; } } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/config/services/environment-config-provider.service.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview Environment Configuration Provider * Extracts configuration from environment variables */ import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; /** * Environment variable mapping definition */ interface EnvMapping { /** Environment variable name */ env: string; /** Path in configuration object */ path: readonly string[]; /** Optional validator function */ validate?: (value: string) => boolean; /** Whether this is runtime state (not configuration) */ isRuntimeState?: boolean; } /** * EnvironmentConfigProvider extracts configuration from environment variables * Single responsibility: Environment variable configuration extraction */ export class EnvironmentConfigProvider { /** * Default environment variable mappings */ private static readonly DEFAULT_MAPPINGS: EnvMapping[] = [ { env: 'TASKMASTER_STORAGE_TYPE', path: ['storage', 'type'], validate: (v: string) => ['file', 'api'].includes(v) }, { env: 'TASKMASTER_API_ENDPOINT', path: ['storage', 'apiEndpoint'] }, { env: 'TASKMASTER_API_TOKEN', path: ['storage', 'apiAccessToken'] }, { env: 'TASKMASTER_MODEL_MAIN', path: ['models', 'main'] }, { env: 'TASKMASTER_MODEL_RESEARCH', path: ['models', 'research'] }, { env: 'TASKMASTER_MODEL_FALLBACK', path: ['models', 'fallback'] }, { env: 'TASKMASTER_RESPONSE_LANGUAGE', path: ['custom', 'responseLanguage'] } ]; /** * Runtime state mappings (separate from configuration) */ private static readonly RUNTIME_STATE_MAPPINGS: EnvMapping[] = [ { env: 'TASKMASTER_TAG', path: ['activeTag'], isRuntimeState: true } ]; private mappings: EnvMapping[]; constructor(customMappings?: EnvMapping[]) { this.mappings = customMappings || [ ...EnvironmentConfigProvider.DEFAULT_MAPPINGS, ...EnvironmentConfigProvider.RUNTIME_STATE_MAPPINGS ]; } /** * Load configuration from environment variables */ loadConfig(): PartialConfiguration { const config: PartialConfiguration = {}; for (const mapping of this.mappings) { // Skip runtime state variables if (mapping.isRuntimeState) continue; const value = process.env[mapping.env]; if (!value) continue; // Validate value if validator is provided if (mapping.validate && !mapping.validate(value)) { console.warn(`Invalid value for ${mapping.env}: ${value}`); continue; } // Set the value in the config object this.setNestedProperty(config, mapping.path, value); } return config; } /** * Get runtime state from environment variables */ getRuntimeState(): Record<string, string> { const state: Record<string, string> = {}; for (const mapping of this.mappings) { if (!mapping.isRuntimeState) continue; const value = process.env[mapping.env]; if (value) { const key = mapping.path[mapping.path.length - 1]; state[key] = value; } } return state; } /** * Helper to set a nested property in an object */ private setNestedProperty( obj: any, path: readonly string[], value: any ): void { const lastKey = path[path.length - 1]; const keys = path.slice(0, -1); let current = obj; for (const key of keys) { if (!current[key]) { current[key] = {}; } current = current[key]; } current[lastKey] = value; } /** * Check if an environment variable is set */ hasEnvVar(envName: string): boolean { return envName in process.env && process.env[envName] !== undefined; } /** * Get all environment variables that match our prefix */ getAllTaskmasterEnvVars(): Record<string, string> { const vars: Record<string, string> = {}; const prefix = 'TASKMASTER_'; for (const [key, value] of Object.entries(process.env)) { if (key.startsWith(prefix) && value !== undefined) { vars[key] = value; } } return vars; } /** * Add a custom mapping */ addMapping(mapping: EnvMapping): void { this.mappings.push(mapping); } /** * Get current mappings */ getMappings(): EnvMapping[] { return [...this.mappings]; } } ``` -------------------------------------------------------------------------------- /mcp-server/src/custom-sdk/schema-converter.js: -------------------------------------------------------------------------------- ```javascript /** * @fileoverview Schema conversion utilities for MCP AI SDK provider */ /** * Convert Zod schema to human-readable JSON instructions * @param {import('zod').ZodSchema} schema - Zod schema object * @param {string} [objectName='result'] - Name of the object being generated * @returns {string} Instructions for JSON generation */ export function convertSchemaToInstructions(schema, objectName = 'result') { try { // Generate example structure from schema const exampleStructure = generateExampleFromSchema(schema); return ` CRITICAL JSON GENERATION INSTRUCTIONS: You must respond with ONLY valid JSON that matches this exact structure for "${objectName}": ${JSON.stringify(exampleStructure, null, 2)} STRICT REQUIREMENTS: 1. Response must start with { and end with } 2. Use double quotes for all strings and property names 3. Do not include any text before or after the JSON 4. Do not wrap in markdown code blocks 5. Do not include explanations or comments 6. Follow the exact property names and types shown above 7. All required fields must be present Begin your response immediately with the opening brace {`; } catch (error) { // Fallback to basic JSON instructions if schema parsing fails return ` CRITICAL JSON GENERATION INSTRUCTIONS: You must respond with ONLY valid JSON for "${objectName}". STRICT REQUIREMENTS: 1. Response must start with { and end with } 2. Use double quotes for all strings and property names 3. Do not include any text before or after the JSON 4. Do not wrap in markdown code blocks 5. Do not include explanations or comments Begin your response immediately with the opening brace {`; } } /** * Generate example structure from Zod schema * @param {import('zod').ZodSchema} schema - Zod schema * @returns {any} Example object matching the schema */ function generateExampleFromSchema(schema) { // This is a simplified schema-to-example converter // For production, you might want to use a more sophisticated library if (!schema || typeof schema._def === 'undefined') { return {}; } const def = schema._def; switch (def.typeName) { case 'ZodObject': const result = {}; const shape = def.shape(); for (const [key, fieldSchema] of Object.entries(shape)) { result[key] = generateExampleFromSchema(fieldSchema); } return result; case 'ZodString': return 'string'; case 'ZodNumber': return 0; case 'ZodBoolean': return false; case 'ZodArray': const elementExample = generateExampleFromSchema(def.type); return [elementExample]; case 'ZodOptional': return generateExampleFromSchema(def.innerType); case 'ZodNullable': return generateExampleFromSchema(def.innerType); case 'ZodEnum': return def.values[0] || 'enum_value'; case 'ZodLiteral': return def.value; case 'ZodUnion': // Use the first option from the union if (def.options && def.options.length > 0) { return generateExampleFromSchema(def.options[0]); } return 'union_value'; case 'ZodRecord': return { key: generateExampleFromSchema(def.valueType) }; default: // For unknown types, return a placeholder return `<${def.typeName || 'unknown'}>`; } } /** * Enhance prompt with JSON generation instructions * @param {Array} prompt - AI SDK prompt array * @param {string} jsonInstructions - JSON generation instructions * @returns {Array} Enhanced prompt array */ export function enhancePromptForJSON(prompt, jsonInstructions) { const enhancedPrompt = [...prompt]; // Find system message or create one let systemMessageIndex = enhancedPrompt.findIndex( (msg) => msg.role === 'system' ); if (systemMessageIndex >= 0) { // Append to existing system message const currentContent = enhancedPrompt[systemMessageIndex].content; enhancedPrompt[systemMessageIndex] = { ...enhancedPrompt[systemMessageIndex], content: currentContent + '\n\n' + jsonInstructions }; } else { // Add new system message at the beginning enhancedPrompt.unshift({ role: 'system', content: jsonInstructions }); } return enhancedPrompt; } ``` -------------------------------------------------------------------------------- /.claude/TM_COMMANDS_GUIDE.md: -------------------------------------------------------------------------------- ```markdown # Task Master Commands for Claude Code Complete guide to using Task Master through Claude Code's slash commands. ## Overview All Task Master functionality is available through the `/project:tm/` namespace with natural language support and intelligent features. ## Quick Start ```bash # Install Task Master /project:tm/setup/quick-install # Initialize project /project:tm/init/quick # Parse requirements /project:tm/parse-prd requirements.md # Start working /project:tm/next ``` ## Command Structure Commands are organized hierarchically to match Task Master's CLI: - Main commands at `/project:tm/[command]` - Subcommands for specific operations `/project:tm/[command]/[subcommand]` - Natural language arguments accepted throughout ## Complete Command Reference ### Setup & Configuration - `/project:tm/setup/install` - Full installation guide - `/project:tm/setup/quick-install` - One-line install - `/project:tm/init` - Initialize project - `/project:tm/init/quick` - Quick init with -y - `/project:tm/models` - View AI config - `/project:tm/models/setup` - Configure AI ### Task Generation - `/project:tm/parse-prd` - Generate from PRD - `/project:tm/parse-prd/with-research` - Enhanced parsing - `/project:tm/generate` - Create task files ### Task Management - `/project:tm/list` - List with natural language filters - `/project:tm/list/with-subtasks` - Hierarchical view - `/project:tm/list/by-status <status>` - Filter by status - `/project:tm/show <id>` - Task details - `/project:tm/add-task` - Create task - `/project:tm/update` - Update tasks - `/project:tm/remove-task` - Delete task ### Status Management - `/project:tm/set-status/to-pending <id>` - `/project:tm/set-status/to-in-progress <id>` - `/project:tm/set-status/to-done <id>` - `/project:tm/set-status/to-review <id>` - `/project:tm/set-status/to-deferred <id>` - `/project:tm/set-status/to-cancelled <id>` ### Task Analysis - `/project:tm/analyze-complexity` - AI analysis - `/project:tm/complexity-report` - View report - `/project:tm/expand <id>` - Break down task - `/project:tm/expand/all` - Expand all complex ### Dependencies - `/project:tm/add-dependency` - Add dependency - `/project:tm/remove-dependency` - Remove dependency - `/project:tm/validate-dependencies` - Check issues - `/project:tm/fix-dependencies` - Auto-fix ### Workflows - `/project:tm/workflows/smart-flow` - Adaptive workflows - `/project:tm/workflows/pipeline` - Chain commands - `/project:tm/workflows/auto-implement` - AI implementation ### Utilities - `/project:tm/status` - Project dashboard - `/project:tm/next` - Next task recommendation - `/project:tm/utils/analyze` - Project analysis - `/project:tm/learn` - Interactive help ## Key Features ### Natural Language Support All commands understand natural language: ``` /project:tm/list pending high priority /project:tm/update mark 23 as done /project:tm/add-task implement OAuth login ``` ### Smart Context Commands analyze project state and provide intelligent suggestions based on: - Current task status - Dependencies - Team patterns - Project phase ### Visual Enhancements - Progress bars and indicators - Status badges - Organized displays - Clear hierarchies ## Common Workflows ### Daily Development ``` /project:tm/workflows/smart-flow morning /project:tm/next /project:tm/set-status/to-in-progress <id> /project:tm/set-status/to-done <id> ``` ### Task Breakdown ``` /project:tm/show <id> /project:tm/expand <id> /project:tm/list/with-subtasks ``` ### Sprint Planning ``` /project:tm/analyze-complexity /project:tm/workflows/pipeline init → expand/all → status ``` ## Migration from Old Commands | Old | New | |-----|-----| | `/project:task-master:list` | `/project:tm/list` | | `/project:task-master:complete` | `/project:tm/set-status/to-done` | | `/project:workflows:auto-implement` | `/project:tm/workflows/auto-implement` | ## Tips 1. Use `/project:tm/` + Tab for command discovery 2. Natural language is supported everywhere 3. Commands provide smart defaults 4. Chain commands for automation 5. Check `/project:tm/learn` for interactive help ``` -------------------------------------------------------------------------------- /assets/claude/TM_COMMANDS_GUIDE.md: -------------------------------------------------------------------------------- ```markdown # Task Master Commands for Claude Code Complete guide to using Task Master through Claude Code's slash commands. ## Overview All Task Master functionality is available through the `/project:tm/` namespace with natural language support and intelligent features. ## Quick Start ```bash # Install Task Master /project:tm/setup/quick-install # Initialize project /project:tm/init/quick # Parse requirements /project:tm/parse-prd requirements.md # Start working /project:tm/next ``` ## Command Structure Commands are organized hierarchically to match Task Master's CLI: - Main commands at `/project:tm/[command]` - Subcommands for specific operations `/project:tm/[command]/[subcommand]` - Natural language arguments accepted throughout ## Complete Command Reference ### Setup & Configuration - `/project:tm/setup/install` - Full installation guide - `/project:tm/setup/quick-install` - One-line install - `/project:tm/init` - Initialize project - `/project:tm/init/quick` - Quick init with -y - `/project:tm/models` - View AI config - `/project:tm/models/setup` - Configure AI ### Task Generation - `/project:tm/parse-prd` - Generate from PRD - `/project:tm/parse-prd/with-research` - Enhanced parsing - `/project:tm/generate` - Create task files ### Task Management - `/project:tm/list` - List with natural language filters - `/project:tm/list/with-subtasks` - Hierarchical view - `/project:tm/list/by-status <status>` - Filter by status - `/project:tm/show <id>` - Task details - `/project:tm/add-task` - Create task - `/project:tm/update` - Update tasks - `/project:tm/remove-task` - Delete task ### Status Management - `/project:tm/set-status/to-pending <id>` - `/project:tm/set-status/to-in-progress <id>` - `/project:tm/set-status/to-done <id>` - `/project:tm/set-status/to-review <id>` - `/project:tm/set-status/to-deferred <id>` - `/project:tm/set-status/to-cancelled <id>` ### Task Analysis - `/project:tm/analyze-complexity` - AI analysis - `/project:tm/complexity-report` - View report - `/project:tm/expand <id>` - Break down task - `/project:tm/expand/all` - Expand all complex ### Dependencies - `/project:tm/add-dependency` - Add dependency - `/project:tm/remove-dependency` - Remove dependency - `/project:tm/validate-dependencies` - Check issues - `/project:tm/fix-dependencies` - Auto-fix ### Workflows - `/project:tm/workflows/smart-flow` - Adaptive workflows - `/project:tm/workflows/pipeline` - Chain commands - `/project:tm/workflows/auto-implement` - AI implementation ### Utilities - `/project:tm/status` - Project dashboard - `/project:tm/next` - Next task recommendation - `/project:tm/utils/analyze` - Project analysis - `/project:tm/learn` - Interactive help ## Key Features ### Natural Language Support All commands understand natural language: ``` /project:tm/list pending high priority /project:tm/update mark 23 as done /project:tm/add-task implement OAuth login ``` ### Smart Context Commands analyze project state and provide intelligent suggestions based on: - Current task status - Dependencies - Team patterns - Project phase ### Visual Enhancements - Progress bars and indicators - Status badges - Organized displays - Clear hierarchies ## Common Workflows ### Daily Development ``` /project:tm/workflows/smart-flow morning /project:tm/next /project:tm/set-status/to-in-progress <id> /project:tm/set-status/to-done <id> ``` ### Task Breakdown ``` /project:tm/show <id> /project:tm/expand <id> /project:tm/list/with-subtasks ``` ### Sprint Planning ``` /project:tm/analyze-complexity /project:tm/workflows/pipeline init → expand/all → status ``` ## Migration from Old Commands | Old | New | |-----|-----| | `/project:task-master:list` | `/project:tm/list` | | `/project:task-master:complete` | `/project:tm/set-status/to-done` | | `/project:workflows:auto-implement` | `/project:tm/workflows/auto-implement` | ## Tips 1. Use `/project:tm/` + Tab for command discovery 2. Natural language is supported everywhere 3. Commands provide smart defaults 4. Chain commands for automation 5. Check `/project:tm/learn` for interactive help ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/clear-subtasks.js: -------------------------------------------------------------------------------- ```javascript /** * Direct function wrapper for clearSubtasks */ import { clearSubtasks } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, disableSilentMode, readJSON } from '../../../../scripts/modules/utils.js'; import fs from 'fs'; import path from 'path'; /** * Clear subtasks from specified tasks * @param {Object} args - Function arguments * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. * @param {string} [args.id] - Task IDs (comma-separated) to clear subtasks from * @param {boolean} [args.all] - Clear subtasks from all tasks * @param {string} [args.tag] - Tag context to operate on (defaults to current active tag) * @param {string} [args.projectRoot] - Project root path (for MCP/env fallback) * @param {Object} log - Logger object * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>} */ export async function clearSubtasksDirect(args, log) { // Destructure expected args const { tasksJsonPath, id, all, tag, projectRoot } = args; try { log.info(`Clearing subtasks with args: ${JSON.stringify(args)}`); // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('clearSubtasksDirect called without tasksJsonPath'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Either id or all must be provided if (!id && !all) { return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: 'Either task IDs with id parameter or all parameter must be provided' } }; } // Use provided path const tasksPath = tasksJsonPath; // Check if tasks.json exists if (!fs.existsSync(tasksPath)) { return { success: false, error: { code: 'FILE_NOT_FOUND_ERROR', message: `Tasks file not found at ${tasksPath}` } }; } let taskIds; // Use readJSON which handles silent migration and tag resolution const data = readJSON(tasksPath, projectRoot, tag); if (!data || !data.tasks) { return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: `No tasks found in tasks file: ${tasksPath}` } }; } const currentTag = data.tag || tag; const tasks = data.tasks; // If all is specified, get all task IDs if (all) { log.info(`Clearing subtasks from all tasks in tag '${currentTag}'`); if (tasks.length === 0) { return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: `No tasks found in tag context '${currentTag}'` } }; } taskIds = tasks.map((t) => t.id).join(','); } else { // Use the provided task IDs taskIds = id; } log.info(`Clearing subtasks from tasks: ${taskIds} in tag '${currentTag}'`); // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); // Call the core function clearSubtasks(tasksPath, taskIds, { projectRoot, tag: currentTag }); // Restore normal logging disableSilentMode(); // Read the updated data to provide a summary const updatedData = readJSON(tasksPath, projectRoot, currentTag); const taskIdArray = taskIds.split(',').map((id) => parseInt(id.trim(), 10)); // Build a summary of what was done const clearedTasksCount = taskIdArray.length; const updatedTasks = updatedData.tasks || []; const taskSummary = taskIdArray.map((id) => { const task = updatedTasks.find((t) => t.id === id); return task ? { id, title: task.title } : { id, title: 'Task not found' }; }); return { success: true, data: { message: `Successfully cleared subtasks from ${clearedTasksCount} task(s) in tag '${currentTag}'`, tasksCleared: taskSummary, tag: currentTag } }; } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error in clearSubtasksDirect: ${error.message}`); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/get-task.js: -------------------------------------------------------------------------------- ```javascript /** * tools/get-task.js * Tool to get task details by ID */ import { z } from 'zod'; import { handleApiResult, createErrorResponse, withNormalizedProjectRoot } from './utils.js'; import { showTaskDirect } from '../core/task-master-core.js'; import { findTasksPath, findComplexityReportPath } from '../core/utils/path-utils.js'; import { resolveTag } from '../../../scripts/modules/utils.js'; /** * Custom processor function that removes allTasks from the response * @param {Object} data - The data returned from showTaskDirect * @returns {Object} - The processed data with allTasks removed */ function processTaskResponse(data) { if (!data) return data; // If we have the expected structure with task and allTasks if (typeof data === 'object' && data !== null && data.id && data.title) { // If the data itself looks like the task object, return it return data; } else if (data.task) { return data.task; } // If structure is unexpected, return as is return data; } /** * Register the get-task tool with the MCP server * @param {Object} server - FastMCP server instance */ export function registerShowTaskTool(server) { server.addTool({ name: 'get_task', description: 'Get detailed information about a specific task', parameters: z.object({ id: z .string() .describe( 'Task ID(s) to get (can be comma-separated for multiple tasks)' ), status: z .string() .optional() .describe("Filter subtasks by status (e.g., 'pending', 'done')"), file: z .string() .optional() .describe('Path to the tasks file relative to project root'), complexityReport: z .string() .optional() .describe( 'Path to the complexity report file (relative to project root or absolute)' ), projectRoot: z .string() .describe( 'Absolute path to the project root directory (Optional, usually from session)' ), tag: z.string().optional().describe('Tag context to operate on') }), execute: withNormalizedProjectRoot(async (args, { log, session }) => { const { id, file, status, projectRoot } = args; try { log.info( `Getting task details for ID: ${id}${status ? ` (filtering subtasks by status: ${status})` : ''} in root: ${projectRoot}` ); const resolvedTag = resolveTag({ projectRoot: args.projectRoot, tag: args.tag }); // Resolve the path to tasks.json using the NORMALIZED projectRoot from args let tasksJsonPath; try { tasksJsonPath = findTasksPath( { projectRoot: projectRoot, file: file }, log ); log.info(`Resolved tasks path: ${tasksJsonPath}`); } catch (error) { log.error(`Error finding tasks.json: ${error.message}`); return createErrorResponse( `Failed to find tasks.json: ${error.message}` ); } // Call the direct function, passing the normalized projectRoot // Resolve the path to complexity report let complexityReportPath; try { complexityReportPath = findComplexityReportPath( { projectRoot: projectRoot, complexityReport: args.complexityReport, tag: resolvedTag }, log ); } catch (error) { log.error(`Error finding complexity report: ${error.message}`); } const result = await showTaskDirect( { tasksJsonPath: tasksJsonPath, reportPath: complexityReportPath, // Pass other relevant args id: id, status: status, projectRoot: projectRoot, tag: resolvedTag }, log, { session } ); if (result.success) { log.info(`Successfully retrieved task details for ID: ${args.id}`); } else { log.error(`Failed to get task: ${result.error.message}`); } // Use our custom processor function return handleApiResult( result, log, 'Error retrieving task details', processTaskResponse, projectRoot ); } catch (error) { log.error(`Error in get-task tool: ${error.message}\n${error.stack}`); return createErrorResponse(`Failed to get task: ${error.message}`); } }) }); } ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/remove-subtask.js: -------------------------------------------------------------------------------- ```javascript import path from 'path'; import { log, readJSON, writeJSON } from '../utils.js'; import generateTaskFiles from './generate-task-files.js'; /** * Remove a subtask from its parent task * @param {string} tasksPath - Path to the tasks.json file * @param {string} subtaskId - ID of the subtask to remove in format "parentId.subtaskId" * @param {boolean} convertToTask - Whether to convert the subtask to a standalone task * @param {boolean} generateFiles - Whether to regenerate task files after removing the subtask * @param {Object} context - Context object containing projectRoot and tag information * @param {string} [context.projectRoot] - Project root path * @param {string} [context.tag] - Tag for the task * @returns {Object|null} The removed subtask if convertToTask is true, otherwise null */ async function removeSubtask( tasksPath, subtaskId, convertToTask = false, generateFiles = false, context = {} ) { const { projectRoot, tag } = context; try { log('info', `Removing subtask ${subtaskId}...`); // Read the existing tasks with proper context const data = readJSON(tasksPath, projectRoot, tag); if (!data || !data.tasks) { throw new Error(`Invalid or missing tasks file at ${tasksPath}`); } // Parse the subtask ID (format: "parentId.subtaskId") if (!subtaskId.includes('.')) { throw new Error( `Invalid subtask ID format: ${subtaskId}. Expected format: "parentId.subtaskId"` ); } const [parentIdStr, subtaskIdStr] = subtaskId.split('.'); const parentId = parseInt(parentIdStr, 10); const subtaskIdNum = parseInt(subtaskIdStr, 10); // Find the parent task const parentTask = data.tasks.find((t) => t.id === parentId); if (!parentTask) { throw new Error(`Parent task with ID ${parentId} not found`); } // Check if parent has subtasks if (!parentTask.subtasks || parentTask.subtasks.length === 0) { throw new Error(`Parent task ${parentId} has no subtasks`); } // Find the subtask to remove const subtaskIndex = parentTask.subtasks.findIndex( (st) => st.id === subtaskIdNum ); if (subtaskIndex === -1) { throw new Error(`Subtask ${subtaskId} not found`); } // Get a copy of the subtask before removing it const removedSubtask = { ...parentTask.subtasks[subtaskIndex] }; // Remove the subtask from the parent parentTask.subtasks.splice(subtaskIndex, 1); // If parent has no more subtasks, remove the subtasks array if (parentTask.subtasks.length === 0) { parentTask.subtasks = undefined; } let convertedTask = null; // Convert the subtask to a standalone task if requested if (convertToTask) { log('info', `Converting subtask ${subtaskId} to a standalone task...`); // Find the highest task ID to determine the next ID const highestId = Math.max(...data.tasks.map((t) => t.id)); const newTaskId = highestId + 1; // Create the new task from the subtask convertedTask = { id: newTaskId, title: removedSubtask.title, description: removedSubtask.description || '', details: removedSubtask.details || '', status: removedSubtask.status || 'pending', dependencies: removedSubtask.dependencies || [], priority: parentTask.priority || 'medium' // Inherit priority from parent }; // Add the parent task as a dependency if not already present if (!convertedTask.dependencies.includes(parentId)) { convertedTask.dependencies.push(parentId); } // Add the converted task to the tasks array data.tasks.push(convertedTask); log('info', `Created new task ${newTaskId} from subtask ${subtaskId}`); } else { log('info', `Subtask ${subtaskId} deleted`); } // Write the updated tasks back to the file with proper context writeJSON(tasksPath, data, projectRoot, tag); // Generate task files if requested if (generateFiles) { log('info', 'Regenerating task files...'); await generateTaskFiles(tasksPath, path.dirname(tasksPath), context); } return convertedTask; } catch (error) { log('error', `Error removing subtask: ${error.message}`); throw error; } } export default removeSubtask; ``` -------------------------------------------------------------------------------- /mcp-server/src/tools/index.js: -------------------------------------------------------------------------------- ```javascript /** * tools/index.js * Export all Task Master CLI tools for MCP server */ import { registerListTasksTool } from './get-tasks.js'; import logger from '../logger.js'; import { registerSetTaskStatusTool } from './set-task-status.js'; import { registerParsePRDTool } from './parse-prd.js'; import { registerUpdateTool } from './update.js'; import { registerUpdateTaskTool } from './update-task.js'; import { registerUpdateSubtaskTool } from './update-subtask.js'; import { registerGenerateTool } from './generate.js'; import { registerShowTaskTool } from './get-task.js'; import { registerNextTaskTool } from './next-task.js'; import { registerExpandTaskTool } from './expand-task.js'; import { registerAddTaskTool } from './add-task.js'; import { registerAddSubtaskTool } from './add-subtask.js'; import { registerRemoveSubtaskTool } from './remove-subtask.js'; import { registerAnalyzeProjectComplexityTool } from './analyze.js'; import { registerClearSubtasksTool } from './clear-subtasks.js'; import { registerExpandAllTool } from './expand-all.js'; import { registerRemoveDependencyTool } from './remove-dependency.js'; import { registerValidateDependenciesTool } from './validate-dependencies.js'; import { registerFixDependenciesTool } from './fix-dependencies.js'; import { registerComplexityReportTool } from './complexity-report.js'; import { registerAddDependencyTool } from './add-dependency.js'; import { registerRemoveTaskTool } from './remove-task.js'; import { registerInitializeProjectTool } from './initialize-project.js'; import { registerModelsTool } from './models.js'; import { registerMoveTaskTool } from './move-task.js'; import { registerResponseLanguageTool } from './response-language.js'; import { registerAddTagTool } from './add-tag.js'; import { registerDeleteTagTool } from './delete-tag.js'; import { registerListTagsTool } from './list-tags.js'; import { registerUseTagTool } from './use-tag.js'; import { registerRenameTagTool } from './rename-tag.js'; import { registerCopyTagTool } from './copy-tag.js'; import { registerResearchTool } from './research.js'; import { registerRulesTool } from './rules.js'; import { registerScopeUpTool } from './scope-up.js'; import { registerScopeDownTool } from './scope-down.js'; /** * Register all Task Master tools with the MCP server * @param {Object} server - FastMCP server instance */ export function registerTaskMasterTools(server) { try { // Register each tool in a logical workflow order // Group 1: Initialization & Setup registerInitializeProjectTool(server); registerModelsTool(server); registerRulesTool(server); registerParsePRDTool(server); // Group 2: Task Analysis & Expansion registerAnalyzeProjectComplexityTool(server); registerExpandTaskTool(server); registerExpandAllTool(server); registerScopeUpTool(server); registerScopeDownTool(server); // Group 3: Task Listing & Viewing registerListTasksTool(server); registerShowTaskTool(server); registerNextTaskTool(server); registerComplexityReportTool(server); // Group 4: Task Status & Management registerSetTaskStatusTool(server); registerGenerateTool(server); // Group 5: Task Creation & Modification registerAddTaskTool(server); registerAddSubtaskTool(server); registerUpdateTool(server); registerUpdateTaskTool(server); registerUpdateSubtaskTool(server); registerRemoveTaskTool(server); registerRemoveSubtaskTool(server); registerClearSubtasksTool(server); registerMoveTaskTool(server); // Group 6: Dependency Management registerAddDependencyTool(server); registerRemoveDependencyTool(server); registerValidateDependenciesTool(server); registerFixDependenciesTool(server); registerResponseLanguageTool(server); // Group 7: Tag Management registerListTagsTool(server); registerAddTagTool(server); registerDeleteTagTool(server); registerUseTagTool(server); registerRenameTagTool(server); registerCopyTagTool(server); // Group 8: Research Features registerResearchTool(server); } catch (error) { logger.error(`Error registering Task Master tools: ${error.message}`); throw error; } } export default { registerTaskMasterTools }; ``` -------------------------------------------------------------------------------- /docs/scripts/models-json-to-markdown.js: -------------------------------------------------------------------------------- ```javascript import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const supportedModelsPath = path.join( __dirname, '..', 'modules', 'supported-models.json' ); const outputMarkdownPath = path.join( __dirname, '..', '..', 'docs', 'models.md' ); function formatCost(cost) { if (cost === null || cost === undefined) { return '—'; } return cost; } function formatSweScore(score) { if (score === null || score === undefined || score === 0) { return '—'; } return score.toString(); } function generateMarkdownTable(title, models) { if (!models || models.length === 0) { return `## ${title}\n\nNo models in this category.\n\n`; } let table = `## ${title}\n\n`; table += '| Provider | Model Name | SWE Score | Input Cost | Output Cost |\n'; table += '|---|---|---|---|---|\n'; models.forEach((model) => { table += `| ${model.provider} | ${model.modelName} | ${formatSweScore(model.sweScore)} | ${formatCost(model.inputCost)} | ${formatCost(model.outputCost)} |\n`; }); table += '\n'; return table; } function generateUnsupportedTable(models) { if (!models || models.length === 0) { return '## Unsupported Models\n\nNo unsupported models found.\n\n'; } let table = '## Unsupported Models\n\n'; table += '| Provider | Model Name | Reason |\n'; table += '|---|---|---|\n'; models.forEach((model) => { table += `| ${model.provider} | ${model.modelName} | ${model.reason || '—'} |\n`; }); table += '\n'; return table; } function main() { try { const correctSupportedModelsPath = path.join( __dirname, '..', '..', 'scripts', 'modules', 'supported-models.json' ); const correctOutputMarkdownPath = path.join(__dirname, '..', 'models.md'); const supportedModelsContent = fs.readFileSync( correctSupportedModelsPath, 'utf8' ); const supportedModels = JSON.parse(supportedModelsContent); const mainModels = []; const researchModels = []; const fallbackModels = []; const unsupportedModels = []; for (const provider in supportedModels) { if (Object.hasOwnProperty.call(supportedModels, provider)) { const models = supportedModels[provider]; models.forEach((model) => { const isSupported = model.supported !== false; // default to true if missing if (isSupported) { const modelEntry = { provider: provider, modelName: model.id, sweScore: model.swe_score, inputCost: model.cost_per_1m_tokens ? model.cost_per_1m_tokens.input : null, outputCost: model.cost_per_1m_tokens ? model.cost_per_1m_tokens.output : null }; if (model.allowed_roles && model.allowed_roles.includes('main')) { mainModels.push(modelEntry); } if ( model.allowed_roles && model.allowed_roles.includes('research') ) { researchModels.push(modelEntry); } if ( model.allowed_roles && model.allowed_roles.includes('fallback') ) { fallbackModels.push(modelEntry); } } else { unsupportedModels.push({ provider: provider, modelName: model.id, reason: model.reason || 'Not specified' }); } }); } } const date = new Date(); const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; const formattedDate = `${monthNames[date.getMonth()]} ${date.getDate()}, ${date.getFullYear()}`; let markdownContent = `# Available Models as of ${formattedDate}\n\n`; markdownContent += generateMarkdownTable('Main Models', mainModels); markdownContent += generateMarkdownTable('Research Models', researchModels); markdownContent += generateMarkdownTable('Fallback Models', fallbackModels); markdownContent += generateUnsupportedTable(unsupportedModels); fs.writeFileSync(correctOutputMarkdownPath, markdownContent, 'utf8'); console.log(`Successfully updated ${correctOutputMarkdownPath}`); } catch (error) { console.error('Error transforming models.json to models.md:', error); process.exit(1); } } main(); ``` -------------------------------------------------------------------------------- /tests/unit/profiles/kiro-integration.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; import fs from 'fs'; import path from 'path'; import os from 'os'; // Mock external modules jest.mock('child_process', () => ({ execSync: jest.fn() })); // Mock console methods jest.mock('console', () => ({ log: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), clear: jest.fn() })); describe('Kiro 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('mcp.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 createProjectStructure behavior for Kiro files function mockCreateKiroStructure() { // This function simulates the actual kiro profile creation logic // It explicitly calls the mocked fs methods to ensure consistency with the test environment // Simulate directory creation calls - these will call the mocked mkdirSync fs.mkdirSync(path.join(tempDir, '.kiro'), { recursive: true }); fs.mkdirSync(path.join(tempDir, '.kiro', 'steering'), { recursive: true }); fs.mkdirSync(path.join(tempDir, '.kiro', 'settings'), { recursive: true }); // Create MCP config file at .kiro/settings/mcp.json // This will call the mocked writeFileSync fs.writeFileSync( path.join(tempDir, '.kiro', 'settings', 'mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2) ); // Create kiro rule files in steering directory // All these will call the mocked writeFileSync fs.writeFileSync( path.join(tempDir, '.kiro', 'steering', 'kiro_rules.md'), '# Kiro Rules\n\nKiro-specific rules and instructions.' ); fs.writeFileSync( path.join(tempDir, '.kiro', 'steering', 'dev_workflow.md'), '# Development Workflow\n\nDevelopment workflow instructions.' ); fs.writeFileSync( path.join(tempDir, '.kiro', 'steering', 'self_improve.md'), '# Self Improvement\n\nSelf improvement guidelines.' ); fs.writeFileSync( path.join(tempDir, '.kiro', 'steering', 'taskmaster.md'), '# Task Master\n\nTask Master integration instructions.' ); } test('creates all required .kiro directories', () => { // Act mockCreateKiroStructure(); // Assert expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.kiro'), { recursive: true }); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kiro', 'steering'), { recursive: true } ); expect(fs.mkdirSync).toHaveBeenCalledWith( path.join(tempDir, '.kiro', 'settings'), { recursive: true } ); }); test('creates Kiro mcp.json with mcpServers format', () => { // Act mockCreateKiroStructure(); // Assert expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kiro', 'settings', 'mcp.json'), JSON.stringify({ mcpServers: {} }, null, 2) ); }); test('creates rule files in steering directory', () => { // Act mockCreateKiroStructure(); // Assert expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kiro', 'steering', 'kiro_rules.md'), '# Kiro Rules\n\nKiro-specific rules and instructions.' ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kiro', 'steering', 'dev_workflow.md'), '# Development Workflow\n\nDevelopment workflow instructions.' ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kiro', 'steering', 'self_improve.md'), '# Self Improvement\n\nSelf improvement guidelines.' ); expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, '.kiro', 'steering', 'taskmaster.md'), '# Task Master\n\nTask Master integration instructions.' ); }); }); ``` -------------------------------------------------------------------------------- /.claude/commands/tm/tm-main.md: -------------------------------------------------------------------------------- ```markdown # Task Master Command Reference Comprehensive command structure for Task Master integration with Claude Code. ## Command Organization Commands are organized hierarchically to match Task Master's CLI structure while providing enhanced Claude Code integration. ## Project Setup & Configuration ### `/project:tm/init` - `init-project` - Initialize new project (handles PRD files intelligently) - `init-project-quick` - Quick setup with auto-confirmation (-y flag) ### `/project:tm/models` - `view-models` - View current AI model configuration - `setup-models` - Interactive model configuration - `set-main` - Set primary generation model - `set-research` - Set research model - `set-fallback` - Set fallback model ## Task Generation ### `/project:tm/parse-prd` - `parse-prd` - Generate tasks from PRD document - `parse-prd-with-research` - Enhanced parsing with research mode ### `/project:tm/generate` - `generate-tasks` - Create individual task files from tasks.json ## Task Management ### `/project:tm/list` - `list-tasks` - Smart listing with natural language filters - `list-tasks-with-subtasks` - Include subtasks in hierarchical view - `list-tasks-by-status` - Filter by specific status ### `/project:tm/set-status` - `to-pending` - Reset task to pending - `to-in-progress` - Start working on task - `to-done` - Mark task complete - `to-review` - Submit for review - `to-deferred` - Defer task - `to-cancelled` - Cancel task ### `/project:tm/sync-readme` - `sync-readme` - Export tasks to README.md with formatting ### `/project:tm/update` - `update-task` - Update tasks with natural language - `update-tasks-from-id` - Update multiple tasks from a starting point - `update-single-task` - Update specific task ### `/project:tm/add-task` - `add-task` - Add new task with AI assistance ### `/project:tm/remove-task` - `remove-task` - Remove task with confirmation ## Subtask Management ### `/project:tm/add-subtask` - `add-subtask` - Add new subtask to parent - `convert-task-to-subtask` - Convert existing task to subtask ### `/project:tm/remove-subtask` - `remove-subtask` - Remove subtask (with optional conversion) ### `/project:tm/clear-subtasks` - `clear-subtasks` - Clear subtasks from specific task - `clear-all-subtasks` - Clear all subtasks globally ## Task Analysis & Breakdown ### `/project:tm/analyze-complexity` - `analyze-complexity` - Analyze and generate expansion recommendations ### `/project:tm/complexity-report` - `complexity-report` - Display complexity analysis report ### `/project:tm/expand` - `expand-task` - Break down specific task - `expand-all-tasks` - Expand all eligible tasks - `with-research` - Enhanced expansion ## Task Navigation ### `/project:tm/next` - `next-task` - Intelligent next task recommendation ### `/project:tm/show` - `show-task` - Display detailed task information ### `/project:tm/status` - `project-status` - Comprehensive project dashboard ## Dependency Management ### `/project:tm/add-dependency` - `add-dependency` - Add task dependency ### `/project:tm/remove-dependency` - `remove-dependency` - Remove task dependency ### `/project:tm/validate-dependencies` - `validate-dependencies` - Check for dependency issues ### `/project:tm/fix-dependencies` - `fix-dependencies` - Automatically fix dependency problems ## Workflows & Automation ### `/project:tm/workflows` - `smart-workflow` - Context-aware intelligent workflow execution - `command-pipeline` - Chain multiple commands together - `auto-implement-tasks` - Advanced auto-implementation with code generation ## Utilities ### `/project:tm/utils` - `analyze-project` - Deep project analysis and insights ### `/project:tm/setup` - `install-taskmaster` - Comprehensive installation guide - `quick-install-taskmaster` - One-line global installation ## Usage Patterns ### Natural Language Most commands accept natural language arguments: ``` /project:tm/add-task create user authentication system /project:tm/update mark all API tasks as high priority /project:tm/list show blocked tasks ``` ### ID-Based Commands Commands requiring IDs intelligently parse from $ARGUMENTS: ``` /project:tm/show 45 /project:tm/expand 23 /project:tm/set-status/to-done 67 ``` ### Smart Defaults Commands provide intelligent defaults and suggestions based on context. ``` -------------------------------------------------------------------------------- /assets/claude/commands/tm/tm-main.md: -------------------------------------------------------------------------------- ```markdown # Task Master Command Reference Comprehensive command structure for Task Master integration with Claude Code. ## Command Organization Commands are organized hierarchically to match Task Master's CLI structure while providing enhanced Claude Code integration. ## Project Setup & Configuration ### `/project:tm/init` - `init-project` - Initialize new project (handles PRD files intelligently) - `init-project-quick` - Quick setup with auto-confirmation (-y flag) ### `/project:tm/models` - `view-models` - View current AI model configuration - `setup-models` - Interactive model configuration - `set-main` - Set primary generation model - `set-research` - Set research model - `set-fallback` - Set fallback model ## Task Generation ### `/project:tm/parse-prd` - `parse-prd` - Generate tasks from PRD document - `parse-prd-with-research` - Enhanced parsing with research mode ### `/project:tm/generate` - `generate-tasks` - Create individual task files from tasks.json ## Task Management ### `/project:tm/list` - `list-tasks` - Smart listing with natural language filters - `list-tasks-with-subtasks` - Include subtasks in hierarchical view - `list-tasks-by-status` - Filter by specific status ### `/project:tm/set-status` - `to-pending` - Reset task to pending - `to-in-progress` - Start working on task - `to-done` - Mark task complete - `to-review` - Submit for review - `to-deferred` - Defer task - `to-cancelled` - Cancel task ### `/project:tm/sync-readme` - `sync-readme` - Export tasks to README.md with formatting ### `/project:tm/update` - `update-task` - Update tasks with natural language - `update-tasks-from-id` - Update multiple tasks from a starting point - `update-single-task` - Update specific task ### `/project:tm/add-task` - `add-task` - Add new task with AI assistance ### `/project:tm/remove-task` - `remove-task` - Remove task with confirmation ## Subtask Management ### `/project:tm/add-subtask` - `add-subtask` - Add new subtask to parent - `convert-task-to-subtask` - Convert existing task to subtask ### `/project:tm/remove-subtask` - `remove-subtask` - Remove subtask (with optional conversion) ### `/project:tm/clear-subtasks` - `clear-subtasks` - Clear subtasks from specific task - `clear-all-subtasks` - Clear all subtasks globally ## Task Analysis & Breakdown ### `/project:tm/analyze-complexity` - `analyze-complexity` - Analyze and generate expansion recommendations ### `/project:tm/complexity-report` - `complexity-report` - Display complexity analysis report ### `/project:tm/expand` - `expand-task` - Break down specific task - `expand-all-tasks` - Expand all eligible tasks - `with-research` - Enhanced expansion ## Task Navigation ### `/project:tm/next` - `next-task` - Intelligent next task recommendation ### `/project:tm/show` - `show-task` - Display detailed task information ### `/project:tm/status` - `project-status` - Comprehensive project dashboard ## Dependency Management ### `/project:tm/add-dependency` - `add-dependency` - Add task dependency ### `/project:tm/remove-dependency` - `remove-dependency` - Remove task dependency ### `/project:tm/validate-dependencies` - `validate-dependencies` - Check for dependency issues ### `/project:tm/fix-dependencies` - `fix-dependencies` - Automatically fix dependency problems ## Workflows & Automation ### `/project:tm/workflows` - `smart-workflow` - Context-aware intelligent workflow execution - `command-pipeline` - Chain multiple commands together - `auto-implement-tasks` - Advanced auto-implementation with code generation ## Utilities ### `/project:tm/utils` - `analyze-project` - Deep project analysis and insights ### `/project:tm/setup` - `install-taskmaster` - Comprehensive installation guide - `quick-install-taskmaster` - One-line global installation ## Usage Patterns ### Natural Language Most commands accept natural language arguments: ``` /project:tm/add-task create user authentication system /project:tm/update mark all API tasks as high priority /project:tm/list show blocked tasks ``` ### ID-Based Commands Commands requiring IDs intelligently parse from $ARGUMENTS: ``` /project:tm/show 45 /project:tm/expand 23 /project:tm/set-status/to-done 67 ``` ### Smart Defaults Commands provide intelligent defaults and suggestions based on context. ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/create-tag-from-branch.js: -------------------------------------------------------------------------------- ```javascript /** * create-tag-from-branch.js * Direct function implementation for creating tags from git branches */ import { createTagFromBranch } from '../../../../scripts/modules/task-manager/tag-management.js'; import { getCurrentBranch, isGitRepository } from '../../../../scripts/modules/utils/git-utils.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; import { createLogWrapper } from '../../tools/utils.js'; /** * Direct function wrapper for creating tags from git branches with error handling. * * @param {Object} args - Command arguments * @param {string} args.tasksJsonPath - Path to the tasks.json file (resolved by tool) * @param {string} [args.branchName] - Git branch name (optional, uses current branch if not provided) * @param {boolean} [args.copyFromCurrent] - Copy tasks from current tag * @param {string} [args.copyFromTag] - Copy tasks from specific tag * @param {string} [args.description] - Custom description for the tag * @param {boolean} [args.autoSwitch] - Automatically switch to the new tag * @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 createTagFromBranchDirect(args, log, context = {}) { // Destructure expected args const { tasksJsonPath, branchName, copyFromCurrent, copyFromTag, description, autoSwitch, 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('createTagFromBranchDirect called without tasksJsonPath'); disableSilentMode(); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Check if projectRoot was provided if (!projectRoot) { log.error('createTagFromBranchDirect called without projectRoot'); disableSilentMode(); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'projectRoot is required' } }; } // Check if we're in a git repository if (!(await isGitRepository(projectRoot))) { log.error('Not in a git repository'); disableSilentMode(); return { success: false, error: { code: 'NOT_GIT_REPOSITORY', message: 'Not in a git repository. Cannot create tag from branch.' } }; } // Determine branch name let targetBranch = branchName; if (!targetBranch) { targetBranch = await getCurrentBranch(projectRoot); if (!targetBranch) { log.error('Could not determine current git branch'); disableSilentMode(); return { success: false, error: { code: 'NO_CURRENT_BRANCH', message: 'Could not determine current git branch' } }; } } log.info(`Creating tag from git branch: ${targetBranch}`); // Prepare options const options = { copyFromCurrent: copyFromCurrent || false, copyFromTag, description: description || `Tag created from git branch "${targetBranch}"`, autoSwitch: autoSwitch || false }; // Call the createTagFromBranch function const result = await createTagFromBranch( tasksJsonPath, targetBranch, options, { session, mcpLog, projectRoot }, 'json' // outputFormat - use 'json' to suppress CLI UI ); // Restore normal logging disableSilentMode(); return { success: true, data: { branchName: result.branchName, tagName: result.tagName, created: result.created, mappingUpdated: result.mappingUpdated, autoSwitched: result.autoSwitched, message: `Successfully created tag "${result.tagName}" from branch "${result.branchName}"` } }; } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error in createTagFromBranchDirect: ${error.message}`); return { success: false, error: { code: error.code || 'CREATE_TAG_FROM_BRANCH_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/add-subtask.js: -------------------------------------------------------------------------------- ```javascript /** * Direct function wrapper for addSubtask */ import { addSubtask } from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, disableSilentMode } from '../../../../scripts/modules/utils.js'; /** * Add a subtask to an existing task * @param {Object} args - Function arguments * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. * @param {string} args.id - Parent task ID * @param {string} [args.taskId] - Existing task ID to convert to subtask (optional) * @param {string} [args.title] - Title for new subtask (when creating a new subtask) * @param {string} [args.description] - Description for new subtask * @param {string} [args.details] - Implementation details for new subtask * @param {string} [args.status] - Status for new subtask (default: 'pending') * @param {string} [args.dependencies] - Comma-separated list of dependency IDs * @param {boolean} [args.skipGenerate] - Skip regenerating task files * @param {string} [args.projectRoot] - Project root directory * @param {string} [args.tag] - Tag for the task * @param {Object} log - Logger object * @returns {Promise<{success: boolean, data?: Object, error?: string}>} */ export async function addSubtaskDirect(args, log) { // Destructure expected args const { tasksJsonPath, id, taskId, title, description, details, status, dependencies: dependenciesStr, skipGenerate, projectRoot, tag } = args; try { log.info(`Adding subtask with args: ${JSON.stringify(args)}`); // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('addSubtaskDirect called without tasksJsonPath'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } if (!id) { return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: 'Parent task ID is required' } }; } // Either taskId or title must be provided if (!taskId && !title) { return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: 'Either taskId or title must be provided' } }; } // Use provided path const tasksPath = tasksJsonPath; // Parse dependencies if provided let dependencies = []; if (dependenciesStr) { dependencies = dependenciesStr.split(',').map((depId) => { // Handle both regular IDs and dot notation return depId.includes('.') ? depId.trim() : parseInt(depId.trim(), 10); }); } // Convert existingTaskId to a number if provided const existingTaskId = taskId ? parseInt(taskId, 10) : null; // Convert parent ID to a number const parentId = parseInt(id, 10); // Determine if we should generate files const generateFiles = !skipGenerate; // Enable silent mode to prevent console logs from interfering with JSON response enableSilentMode(); const context = { projectRoot, tag }; // Case 1: Convert existing task to subtask if (existingTaskId) { log.info(`Converting task ${existingTaskId} to a subtask of ${parentId}`); const result = await addSubtask( tasksPath, parentId, existingTaskId, null, generateFiles, context ); // Restore normal logging disableSilentMode(); return { success: true, data: { message: `Task ${existingTaskId} successfully converted to a subtask of task ${parentId}`, subtask: result } }; } // Case 2: Create new subtask else { log.info(`Creating new subtask for parent task ${parentId}`); const newSubtaskData = { title: title, description: description || '', details: details || '', status: status || 'pending', dependencies: dependencies }; const result = await addSubtask( tasksPath, parentId, null, newSubtaskData, generateFiles, context ); // Restore normal logging disableSilentMode(); return { success: true, data: { message: `New subtask ${parentId}.${result.id} successfully created`, subtask: result } }; } } catch (error) { // Make sure to restore normal logging even if there's an error disableSilentMode(); log.error(`Error in addSubtaskDirect: ${error.message}`); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/show-task.js: -------------------------------------------------------------------------------- ```javascript /** * show-task.js * Direct function implementation for showing task details */ import { findTaskById, readComplexityReport, readJSON } from '../../../../scripts/modules/utils.js'; import { findTasksPath } from '../utils/path-utils.js'; /** * Direct function wrapper for getting task details. * * @param {Object} args - Command arguments. * @param {string} args.id - Task ID to show. * @param {string} [args.file] - Optional path to the tasks file (passed to findTasksPath). * @param {string} args.reportPath - Explicit path to the complexity report file. * @param {string} [args.status] - Optional status to filter subtasks by. * @param {string} args.projectRoot - Absolute path to the project root directory (already normalized by tool). * @param {string} [args.tag] - Tag for the task * @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 showTaskDirect(args, log) { // This function doesn't need session context since it only reads data // Destructure projectRoot and other args. projectRoot is assumed normalized. const { id, file, reportPath, status, projectRoot, tag } = args; log.info( `Showing task direct function. ID: ${id}, File: ${file}, Status Filter: ${status}, ProjectRoot: ${projectRoot}` ); // --- Path Resolution using the passed (already normalized) projectRoot --- let tasksJsonPath; try { // Use the projectRoot passed directly from args tasksJsonPath = findTasksPath( { projectRoot: projectRoot, file: file }, log ); log.info(`Resolved tasks path: ${tasksJsonPath}`); } catch (error) { log.error(`Error finding tasks.json: ${error.message}`); return { success: false, error: { code: 'TASKS_FILE_NOT_FOUND', message: `Failed to find tasks.json: ${error.message}` } }; } // --- End Path Resolution --- // --- Rest of the function remains the same, using tasksJsonPath --- try { const tasksData = readJSON(tasksJsonPath, projectRoot, tag); if (!tasksData || !tasksData.tasks) { return { success: false, error: { code: 'INVALID_TASKS_DATA', message: 'Invalid tasks data' } }; } const complexityReport = readComplexityReport(reportPath); // Parse comma-separated IDs const taskIds = id .split(',') .map((taskId) => taskId.trim()) .filter((taskId) => taskId.length > 0); if (taskIds.length === 0) { return { success: false, error: { code: 'INVALID_TASK_ID', message: 'No valid task IDs provided' } }; } // Handle single task ID (existing behavior) if (taskIds.length === 1) { const { task, originalSubtaskCount } = findTaskById( tasksData.tasks, taskIds[0], complexityReport, status ); if (!task) { return { success: false, error: { code: 'TASK_NOT_FOUND', message: `Task or subtask with ID ${taskIds[0]} not found` } }; } log.info(`Successfully retrieved task ${taskIds[0]}.`); const returnData = { ...task }; if (originalSubtaskCount !== null) { returnData._originalSubtaskCount = originalSubtaskCount; returnData._subtaskFilter = status; } return { success: true, data: returnData }; } // Handle multiple task IDs const foundTasks = []; const notFoundIds = []; taskIds.forEach((taskId) => { const { task, originalSubtaskCount } = findTaskById( tasksData.tasks, taskId, complexityReport, status ); if (task) { const taskData = { ...task }; if (originalSubtaskCount !== null) { taskData._originalSubtaskCount = originalSubtaskCount; taskData._subtaskFilter = status; } foundTasks.push(taskData); } else { notFoundIds.push(taskId); } }); log.info( `Successfully retrieved ${foundTasks.length} of ${taskIds.length} requested tasks.` ); // Return multiple tasks with metadata return { success: true, data: { tasks: foundTasks, requestedIds: taskIds, foundCount: foundTasks.length, notFoundIds: notFoundIds, isMultiple: true } }; } catch (error) { log.error(`Error showing task ${id}: ${error.message}`); return { success: false, error: { code: 'TASK_OPERATION_ERROR', message: error.message } }; } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/context-manager.js: -------------------------------------------------------------------------------- ```javascript /** * context-manager.js * Context and cache management for Task Master MCP Server */ import { FastMCP } from 'fastmcp'; import { LRUCache } from 'lru-cache'; /** * Configuration options for the ContextManager * @typedef {Object} ContextManagerConfig * @property {number} maxCacheSize - Maximum number of items in the cache * @property {number} ttl - Time to live for cached items in milliseconds * @property {number} maxContextSize - Maximum size of context window in tokens */ export class ContextManager { /** * Create a new ContextManager instance * @param {ContextManagerConfig} config - Configuration options */ constructor(config = {}) { this.config = { maxCacheSize: config.maxCacheSize || 1000, ttl: config.ttl || 1000 * 60 * 5, // 5 minutes default maxContextSize: config.maxContextSize || 4000 }; // Initialize LRU cache for context data this.cache = new LRUCache({ max: this.config.maxCacheSize, ttl: this.config.ttl, updateAgeOnGet: true }); // Cache statistics this.stats = { hits: 0, misses: 0, invalidations: 0 }; } /** * Create a new context or retrieve from cache * @param {string} contextId - Unique identifier for the context * @param {Object} metadata - Additional metadata for the context * @returns {Object} Context object with metadata */ async getContext(contextId, metadata = {}) { const cacheKey = this._getCacheKey(contextId, metadata); // Try to get from cache first const cached = this.cache.get(cacheKey); if (cached) { this.stats.hits++; return cached; } this.stats.misses++; // Create new context if not in cache const context = { id: contextId, metadata: { ...metadata, created: new Date().toISOString() } }; // Cache the new context this.cache.set(cacheKey, context); return context; } /** * Update an existing context * @param {string} contextId - Context identifier * @param {Object} updates - Updates to apply to the context * @returns {Object} Updated context */ async updateContext(contextId, updates) { const context = await this.getContext(contextId); // Apply updates to context Object.assign(context.metadata, updates); // Update cache const cacheKey = this._getCacheKey(contextId, context.metadata); this.cache.set(cacheKey, context); return context; } /** * Invalidate a context in the cache * @param {string} contextId - Context identifier * @param {Object} metadata - Metadata used in the cache key */ invalidateContext(contextId, metadata = {}) { const cacheKey = this._getCacheKey(contextId, metadata); this.cache.delete(cacheKey); this.stats.invalidations++; } /** * Get cached data associated with a specific key. * Increments cache hit stats if found. * @param {string} key - The cache key. * @returns {any | undefined} The cached data or undefined if not found/expired. */ getCachedData(key) { const cached = this.cache.get(key); if (cached !== undefined) { // Check for undefined specifically, as null/false might be valid cached values this.stats.hits++; return cached; } this.stats.misses++; return undefined; } /** * Set data in the cache with a specific key. * @param {string} key - The cache key. * @param {any} data - The data to cache. */ setCachedData(key, data) { this.cache.set(key, data); } /** * Invalidate a specific cache key. * Increments invalidation stats. * @param {string} key - The cache key to invalidate. */ invalidateCacheKey(key) { this.cache.delete(key); this.stats.invalidations++; } /** * Get cache statistics * @returns {Object} Cache statistics */ getStats() { return { hits: this.stats.hits, misses: this.stats.misses, invalidations: this.stats.invalidations, size: this.cache.size, maxSize: this.config.maxCacheSize, ttl: this.config.ttl }; } /** * Generate a cache key from context ID and metadata * @private * @deprecated No longer used for direct cache key generation outside the manager. * Prefer generating specific keys in calling functions. */ _getCacheKey(contextId, metadata) { // Kept for potential backward compatibility or internal use if needed later. return `${contextId}:${JSON.stringify(metadata)}`; } } // Export a singleton instance with default config export const contextManager = new ContextManager(); ``` -------------------------------------------------------------------------------- /.github/scripts/parse-metrics.mjs: -------------------------------------------------------------------------------- ``` #!/usr/bin/env node import { readFileSync, existsSync, writeFileSync } from 'fs'; function parseMetricsTable(content, metricName) { const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); // Match a markdown table row like: | Metric Name | value | ... const safeName = metricName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const re = new RegExp(`^\\|\\s*${safeName}\\s*\\|\\s*([^|]+)\\|?`); const match = line.match(re); if (match) { return match[1].trim() || 'N/A'; } } return 'N/A'; } function parseCountMetric(content, metricName) { const result = parseMetricsTable(content, metricName); // Extract number from string, handling commas and spaces const numberMatch = result.toString().match(/[\d,]+/); if (numberMatch) { const number = parseInt(numberMatch[0].replace(/,/g, '')); return isNaN(number) ? 0 : number; } return 0; } function main() { const metrics = { issues_created: 0, issues_closed: 0, prs_created: 0, prs_merged: 0, issue_avg_first_response: 'N/A', issue_avg_time_to_close: 'N/A', pr_avg_first_response: 'N/A', pr_avg_merge_time: 'N/A' }; // Parse issue metrics if (existsSync('issue_metrics.md')) { console.log('📄 Found issue_metrics.md, parsing...'); const issueContent = readFileSync('issue_metrics.md', 'utf8'); metrics.issues_created = parseCountMetric( issueContent, 'Total number of items created' ); metrics.issues_closed = parseCountMetric( issueContent, 'Number of items closed' ); metrics.issue_avg_first_response = parseMetricsTable( issueContent, 'Time to first response' ); metrics.issue_avg_time_to_close = parseMetricsTable( issueContent, 'Time to close' ); } else { console.warn('[parse-metrics] issue_metrics.md not found; using defaults.'); } // Parse PR created metrics if (existsSync('pr_created_metrics.md')) { console.log('📄 Found pr_created_metrics.md, parsing...'); const prCreatedContent = readFileSync('pr_created_metrics.md', 'utf8'); metrics.prs_created = parseCountMetric( prCreatedContent, 'Total number of items created' ); metrics.pr_avg_first_response = parseMetricsTable( prCreatedContent, 'Time to first response' ); } else { console.warn( '[parse-metrics] pr_created_metrics.md not found; using defaults.' ); } // Parse PR merged metrics (for more accurate merge data) if (existsSync('pr_merged_metrics.md')) { console.log('📄 Found pr_merged_metrics.md, parsing...'); const prMergedContent = readFileSync('pr_merged_metrics.md', 'utf8'); metrics.prs_merged = parseCountMetric( prMergedContent, 'Total number of items created' ); // For merged PRs, "Time to close" is actually time to merge metrics.pr_avg_merge_time = parseMetricsTable( prMergedContent, 'Time to close' ); } else { console.warn( '[parse-metrics] pr_merged_metrics.md not found; falling back to pr_metrics.md.' ); // Fallback: try old pr_metrics.md if it exists if (existsSync('pr_metrics.md')) { console.log('📄 Falling back to pr_metrics.md...'); const prContent = readFileSync('pr_metrics.md', 'utf8'); const mergedCount = parseCountMetric(prContent, 'Number of items merged'); metrics.prs_merged = mergedCount || parseCountMetric(prContent, 'Number of items closed'); const maybeMergeTime = parseMetricsTable( prContent, 'Average time to merge' ); metrics.pr_avg_merge_time = maybeMergeTime !== 'N/A' ? maybeMergeTime : parseMetricsTable(prContent, 'Time to close'); } else { console.warn('[parse-metrics] pr_metrics.md not found; using defaults.'); } } // Output for GitHub Actions const output = Object.entries(metrics) .map(([key, value]) => `${key}=${value}`) .join('\n'); // Always output to stdout for debugging console.log('\n=== FINAL METRICS ==='); Object.entries(metrics).forEach(([key, value]) => { console.log(`${key}: ${value}`); }); // Write to GITHUB_OUTPUT if in GitHub Actions if (process.env.GITHUB_OUTPUT) { try { writeFileSync(process.env.GITHUB_OUTPUT, output + '\n', { flag: 'a' }); console.log( `\nSuccessfully wrote metrics to ${process.env.GITHUB_OUTPUT}` ); } catch (error) { console.error(`Failed to write to GITHUB_OUTPUT: ${error.message}`); process.exit(1); } } else { console.log( '\nNo GITHUB_OUTPUT environment variable found, skipping file write' ); } } main(); ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/components/ToastNotification.tsx: -------------------------------------------------------------------------------- ```typescript /** * Toast Notification Component */ import React, { useState, useEffect } from 'react'; import type { ToastNotification as ToastType } from '../types'; interface ToastNotificationProps { notification: ToastType; onDismiss: (id: string) => void; } export const ToastNotification: React.FC<ToastNotificationProps> = ({ notification, onDismiss }) => { const [isVisible, setIsVisible] = useState(true); const [progress, setProgress] = useState(100); const duration = notification.duration || 5000; // 5 seconds default useEffect(() => { const progressInterval = setInterval(() => { setProgress((prev) => { const decrease = (100 / duration) * 100; // Update every 100ms return Math.max(0, prev - decrease); }); }, 100); const timeoutId = setTimeout(() => { setIsVisible(false); setTimeout(() => onDismiss(notification.id), 300); // Wait for animation }, duration); return () => { clearInterval(progressInterval); clearTimeout(timeoutId); }; }, [notification.id, duration, onDismiss]); const getIcon = () => { switch (notification.type) { case 'success': return ( <svg className="w-5 h-5 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" /> </svg> ); case 'info': return ( <svg className="w-5 h-5 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> ); case 'warning': return ( <svg className="w-5 h-5 text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.667-2.308-1.667-3.08 0L3.34 19c-.77 1.333.192 3 1.732 3z" /> </svg> ); case 'error': return ( <svg className="w-5 h-5 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> ); } }; const bgColor = { success: 'bg-green-900/90', info: 'bg-blue-900/90', warning: 'bg-yellow-900/90', error: 'bg-red-900/90' }[notification.type]; const borderColor = { success: 'border-green-600', info: 'border-blue-600', warning: 'border-yellow-600', error: 'border-red-600' }[notification.type]; const progressColor = { success: 'bg-green-400', info: 'bg-blue-400', warning: 'bg-yellow-400', error: 'bg-red-400' }[notification.type]; return ( <div className={`${bgColor} ${borderColor} border rounded-lg shadow-lg p-4 mb-2 transition-all duration-300 ${ isVisible ? 'opacity-100 translate-x-0' : 'opacity-0 translate-x-full' } max-w-sm w-full relative overflow-hidden`} > <div className="flex items-start"> <div className="flex-shrink-0">{getIcon()}</div> <div className="ml-3 flex-1"> <h3 className="text-sm font-medium text-white"> {notification.title} </h3> <p className="mt-1 text-sm text-gray-300">{notification.message}</p> </div> <button onClick={() => onDismiss(notification.id)} className="ml-4 flex-shrink-0 inline-flex text-gray-400 hover:text-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" > <span className="sr-only">Close</span> <svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <path fillRule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clipRule="evenodd" /> </svg> </button> </div> {/* Progress bar */} <div className="absolute bottom-0 left-0 w-full h-1 bg-gray-700"> <div className={`h-full ${progressColor} transition-all duration-100 ease-linear`} style={{ width: `${progress}%` }} /> </div> </div> ); }; ``` -------------------------------------------------------------------------------- /apps/extension/esbuild.js: -------------------------------------------------------------------------------- ```javascript const esbuild = require('esbuild'); const path = require('path'); const production = process.argv.includes('--production'); const watch = process.argv.includes('--watch'); /** * @type {import('esbuild').Plugin} */ const esbuildProblemMatcherPlugin = { name: 'esbuild-problem-matcher', setup(build) { build.onStart(() => { console.log('[watch] build started'); }); build.onEnd((result) => { result.errors.forEach(({ text, location }) => { console.error(`✘ [ERROR] ${text}`); console.error( ` ${location.file}:${location.line}:${location.column}:` ); }); console.log('[watch] build finished'); }); } }; /** * @type {import('esbuild').Plugin} */ const aliasPlugin = { name: 'alias', setup(build) { // Handle @/ aliases for shadcn/ui build.onResolve({ filter: /^@\// }, (args) => { const resolvedPath = path.resolve(__dirname, 'src', args.path.slice(2)); // Try to resolve with common TypeScript extensions const fs = require('fs'); const extensions = ['.tsx', '.ts', '.jsx', '.js']; // Check if it's a file first for (const ext of extensions) { const fullPath = resolvedPath + ext; if (fs.existsSync(fullPath)) { return { path: fullPath }; } } // Check if it's a directory with index file for (const ext of extensions) { const indexPath = path.join(resolvedPath, 'index' + ext); if (fs.existsSync(indexPath)) { return { path: indexPath }; } } // Fallback to original behavior return { path: resolvedPath }; }); } }; async function main() { // Build configuration for the VS Code extension const extensionCtx = await esbuild.context({ entryPoints: ['src/extension.ts'], bundle: true, format: 'cjs', minify: production, sourcemap: !production ? 'inline' : false, sourcesContent: !production, platform: 'node', outdir: 'dist', external: ['vscode'], logLevel: 'silent', // Add production optimizations ...(production && { drop: ['debugger'], pure: ['console.log', 'console.debug', 'console.trace'] }), plugins: [esbuildProblemMatcherPlugin, aliasPlugin] }); // Build configuration for the React webview const webviewCtx = await esbuild.context({ entryPoints: ['src/webview/index.tsx'], bundle: true, format: 'iife', globalName: 'App', minify: production, sourcemap: !production ? 'inline' : false, sourcesContent: !production, platform: 'browser', outdir: 'dist', logLevel: 'silent', target: ['es2020'], jsx: 'automatic', jsxImportSource: 'react', external: ['*.css'], // Bundle React with webview since it's not available in the runtime // This prevents the multiple React instances issue // Ensure React is resolved from the workspace root to avoid duplicates alias: { react: path.resolve(__dirname, '../../node_modules/react'), 'react-dom': path.resolve(__dirname, '../../node_modules/react-dom') }, define: { 'process.env.NODE_ENV': production ? '"production"' : '"development"', global: 'globalThis' }, // Add production optimizations for webview too ...(production && { drop: ['debugger'], pure: ['console.log', 'console.debug', 'console.trace'] }), plugins: [esbuildProblemMatcherPlugin, aliasPlugin] }); // Build configuration for the React sidebar const sidebarCtx = await esbuild.context({ entryPoints: ['src/webview/sidebar.tsx'], bundle: true, format: 'iife', globalName: 'SidebarApp', minify: production, sourcemap: !production ? 'inline' : false, sourcesContent: !production, platform: 'browser', outdir: 'dist', logLevel: 'silent', target: ['es2020'], jsx: 'automatic', jsxImportSource: 'react', external: ['*.css'], alias: { react: path.resolve(__dirname, '../../node_modules/react'), 'react-dom': path.resolve(__dirname, '../../node_modules/react-dom') }, define: { 'process.env.NODE_ENV': production ? '"production"' : '"development"', global: 'globalThis' }, ...(production && { drop: ['debugger'], pure: ['console.log', 'console.debug', 'console.trace'] }), plugins: [esbuildProblemMatcherPlugin, aliasPlugin] }); if (watch) { await Promise.all([ extensionCtx.watch(), webviewCtx.watch(), sidebarCtx.watch() ]); } else { await Promise.all([ extensionCtx.rebuild(), webviewCtx.rebuild(), sidebarCtx.rebuild() ]); await extensionCtx.dispose(); await webviewCtx.dispose(); await sidebarCtx.dispose(); } } main().catch((e) => { console.error(e); process.exit(1); }); ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/setup.js: -------------------------------------------------------------------------------- ```javascript /** * Common setup for task-manager module tests */ import { jest } from '@jest/globals'; // Sample test data export const sampleTasks = { meta: { projectName: 'Test Project' }, tasks: [ { id: 1, title: 'Task 1', description: 'First task description', status: 'pending', dependencies: [], priority: 'high', details: 'Detailed information for task 1', testStrategy: 'Test strategy for task 1' }, { id: 2, title: 'Task 2', description: 'Second task description', status: 'pending', dependencies: [1], priority: 'medium', details: 'Detailed information for task 2', testStrategy: 'Test strategy for task 2' }, { id: 3, title: 'Task with Subtasks', description: 'Task with subtasks description', status: 'pending', dependencies: [1, 2], priority: 'high', details: 'Detailed information for task 3', testStrategy: 'Test strategy for task 3', subtasks: [ { id: 1, title: 'Subtask 1', description: 'First subtask', status: 'pending', dependencies: [], details: 'Details for subtask 1' }, { id: 2, title: 'Subtask 2', description: 'Second subtask', status: 'pending', dependencies: [1], details: 'Details for subtask 2' } ] } ] }; export const emptySampleTasks = { meta: { projectName: 'Empty Project' }, tasks: [] }; export const sampleClaudeResponse = { tasks: [ { id: 1, title: 'Setup Project', description: 'Initialize the project structure', status: 'pending', dependencies: [], priority: 'high', details: 'Create repository, configure build system, and setup dev environment', testStrategy: 'Verify project builds and tests run' }, { id: 2, title: 'Implement Core Feature', description: 'Create the main functionality', status: 'pending', dependencies: [1], priority: 'high', details: 'Implement the core business logic for the application', testStrategy: 'Unit tests for core functions, integration tests for workflows' } ] }; // Common mock setup function export const setupCommonMocks = () => { // Clear mocks before setup jest.clearAllMocks(); // Mock implementations const mocks = { readFileSync: jest.fn(), existsSync: jest.fn(), mkdirSync: jest.fn(), writeFileSync: jest.fn(), readJSON: jest.fn(), writeJSON: jest.fn(), log: jest.fn(), isTaskDependentOn: jest.fn().mockReturnValue(false), formatDependenciesWithStatus: jest.fn(), displayTaskList: jest.fn(), validateAndFixDependencies: jest.fn(), generateObjectService: jest.fn().mockResolvedValue({ mainResult: { tasks: [] }, telemetryData: {} }) }; return mocks; }; // Helper to create a deep copy of objects to avoid test pollution export const cloneData = (data) => JSON.parse(JSON.stringify(data)); /** * Shared mock implementation for getTagAwareFilePath that matches the actual implementation * This ensures consistent behavior across all test files, particularly regarding projectRoot handling. * * The key difference from previous inconsistent implementations was that some tests were not * properly handling the projectRoot parameter, leading to different behaviors between test files. * * @param {string} basePath - The base file path * @param {string|null} tag - The tag name (null, undefined, or 'master' uses base path) * @param {string} [projectRoot='.'] - The project root directory * @returns {string} The resolved file path */ export const createGetTagAwareFilePathMock = () => { return jest.fn((basePath, tag, projectRoot = '.') => { // Handle projectRoot consistently - this was the key fix const fullPath = projectRoot ? `${projectRoot}/${basePath}` : basePath; if (!tag || tag === 'master') { return fullPath; } // Mock the slugification behavior (matches actual implementation) const slugifiedTag = tag.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase(); const idx = fullPath.lastIndexOf('.'); return `${fullPath.slice(0, idx)}_${slugifiedTag}${fullPath.slice(idx)}`; }); }; /** * Shared mock implementation for slugifyTagForFilePath that matches the actual implementation * @param {string} tagName - The tag name to slugify * @returns {string} Slugified tag name safe for filesystem use */ export const createSlugifyTagForFilePathMock = () => { return jest.fn((tagName) => { if (!tagName || typeof tagName !== 'string') { return 'unknown-tag'; } return tagName.replace(/[^a-zA-Z0-9_-]/g, '-').toLowerCase(); }); }; ``` -------------------------------------------------------------------------------- /tests/unit/profiles/gemini-integration.test.js: -------------------------------------------------------------------------------- ```javascript import { jest } from '@jest/globals'; import fs from 'fs'; import path from 'path'; import os from 'os'; // Mock external modules jest.mock('child_process', () => ({ execSync: jest.fn() })); // Mock console methods jest.mock('console', () => ({ log: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), clear: jest.fn() })); describe('Gemini 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 Gemini 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 Gemini profile file copying behavior function mockCreateGeminiStructure() { // Gemini profile copies AGENTS.md to GEMINI.md in project root const sourceContent = 'Sample AGENTS.md content for Gemini integration'; fs.writeFileSync(path.join(tempDir, 'GEMINI.md'), sourceContent); // Gemini profile creates .gemini directory fs.mkdirSync(path.join(tempDir, '.gemini'), { recursive: true }); // Gemini profile creates settings.json in .gemini directory const settingsContent = JSON.stringify( { mcpServers: { 'task-master-ai': { command: 'npx', args: ['-y', 'task-master-ai'], env: { YOUR_ANTHROPIC_API_KEY: 'your-api-key-here', YOUR_PERPLEXITY_API_KEY: 'your-api-key-here', YOUR_OPENAI_API_KEY: 'your-api-key-here', YOUR_GOOGLE_API_KEY: 'your-api-key-here', YOUR_MISTRAL_API_KEY: 'your-api-key-here', YOUR_AZURE_OPENAI_API_KEY: 'your-api-key-here', YOUR_AZURE_OPENAI_ENDPOINT: 'your-endpoint-here', YOUR_OPENROUTER_API_KEY: 'your-api-key-here', YOUR_XAI_API_KEY: 'your-api-key-here', YOUR_OLLAMA_API_KEY: 'your-api-key-here', YOUR_OLLAMA_BASE_URL: 'http://localhost:11434/api', YOUR_AWS_ACCESS_KEY_ID: 'your-access-key-id', YOUR_AWS_SECRET_ACCESS_KEY: 'your-secret-access-key', YOUR_AWS_REGION: 'us-east-1' } } } }, null, 2 ); fs.writeFileSync( path.join(tempDir, '.gemini', 'settings.json'), settingsContent ); } test('creates GEMINI.md file in project root', () => { // Act mockCreateGeminiStructure(); // Assert expect(fs.writeFileSync).toHaveBeenCalledWith( path.join(tempDir, 'GEMINI.md'), 'Sample AGENTS.md content for Gemini integration' ); }); test('creates .gemini profile directory', () => { // Act mockCreateGeminiStructure(); // Assert expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.gemini'), { recursive: true }); }); test('creates MCP configuration as settings.json', () => { // Act mockCreateGeminiStructure(); // Assert - Gemini profile should create settings.json instead of mcp.json const writeFileCalls = fs.writeFileSync.mock.calls; const settingsJsonCall = writeFileCalls.find((call) => call[0].toString().includes('.gemini/settings.json') ); expect(settingsJsonCall).toBeDefined(); }); test('uses settings.json instead of mcp.json', () => { // Act mockCreateGeminiStructure(); // Assert - Should use settings.json, not mcp.json const writeFileCalls = fs.writeFileSync.mock.calls; const mcpJsonCalls = writeFileCalls.filter((call) => call[0].toString().includes('mcp.json') ); expect(mcpJsonCalls).toHaveLength(0); const settingsJsonCalls = writeFileCalls.filter((call) => call[0].toString().includes('settings.json') ); expect(settingsJsonCalls).toHaveLength(1); }); test('renames AGENTS.md to GEMINI.md', () => { // Act mockCreateGeminiStructure(); // Assert - Gemini should rename AGENTS.md to GEMINI.md const writeFileCalls = fs.writeFileSync.mock.calls; const geminiMdCall = writeFileCalls.find((call) => call[0].toString().includes('GEMINI.md') ); expect(geminiMdCall).toBeDefined(); expect(geminiMdCall[0]).toBe(path.join(tempDir, 'GEMINI.md')); }); }); ``` -------------------------------------------------------------------------------- /src/ai-providers/google-vertex.js: -------------------------------------------------------------------------------- ```javascript /** * google-vertex.js * AI provider implementation for Google Vertex AI models using Vercel AI SDK. */ import { createVertex } from '@ai-sdk/google-vertex'; import { BaseAIProvider } from './base-provider.js'; import { resolveEnvVariable } from '../../scripts/modules/utils.js'; import { log } from '../../scripts/modules/utils.js'; // Vertex-specific error classes class VertexAuthError extends Error { constructor(message) { super(message); this.name = 'VertexAuthError'; this.code = 'vertex_auth_error'; } } class VertexConfigError extends Error { constructor(message) { super(message); this.name = 'VertexConfigError'; this.code = 'vertex_config_error'; } } class VertexApiError extends Error { constructor(message, statusCode) { super(message); this.name = 'VertexApiError'; this.code = 'vertex_api_error'; this.statusCode = statusCode; } } export class VertexAIProvider extends BaseAIProvider { constructor() { super(); this.name = 'Google Vertex AI'; } /** * Returns the required API key environment variable name for Google Vertex AI. * @returns {string} The environment variable name */ getRequiredApiKeyName() { return 'GOOGLE_API_KEY'; } /** * Validates Vertex AI-specific authentication parameters * @param {object} params - Parameters to validate * @throws {Error} If required parameters are missing */ validateAuth(params) { const { apiKey, projectId, location, credentials } = params; // Check for API key OR service account credentials if (!apiKey && !credentials) { throw new VertexAuthError( 'Either Google API key (GOOGLE_API_KEY) or service account credentials (GOOGLE_APPLICATION_CREDENTIALS) is required for Vertex AI' ); } // Project ID is required for Vertex AI if (!projectId) { throw new VertexConfigError( 'Google Cloud project ID is required for Vertex AI. Set VERTEX_PROJECT_ID environment variable.' ); } // Location is required for Vertex AI if (!location) { throw new VertexConfigError( 'Google Cloud location is required for Vertex AI. Set VERTEX_LOCATION environment variable (e.g., "us-central1").' ); } } /** * Creates and returns a Google Vertex AI client instance. * @param {object} params - Parameters for client initialization * @param {string} [params.apiKey] - Google API key * @param {string} params.projectId - Google Cloud project ID * @param {string} params.location - Google Cloud location (e.g., "us-central1") * @param {object} [params.credentials] - Service account credentials object * @param {string} [params.baseURL] - Optional custom API endpoint * @returns {Function} Google Vertex AI client function * @throws {Error} If required parameters are missing or initialization fails */ getClient(params) { try { // Validate required parameters this.validateAuth(params); const { apiKey, projectId, location, credentials, baseURL } = params; // Configure auth options - either API key or service account const authOptions = {}; if (apiKey) { authOptions.apiKey = apiKey; } else if (credentials) { authOptions.googleAuthOptions = credentials; } // Return Vertex AI client return createVertex({ ...authOptions, projectId, location, ...(baseURL && { baseURL }) }); } catch (error) { this.handleError('client initialization', error); } } /** * Handle errors from Vertex AI * @param {string} operation - Description of the operation that failed * @param {Error} error - The error object * @throws {Error} Rethrows the error with additional context */ handleError(operation, error) { log('error', `Vertex AI ${operation} error:`, error); // Handle known error types if ( error.name === 'VertexAuthError' || error.name === 'VertexConfigError' || error.name === 'VertexApiError' ) { throw error; } // Handle network/API errors if (error.response) { const statusCode = error.response.status; const errorMessage = error.response.data?.error?.message || error.message; // Categorize by status code if (statusCode === 401 || statusCode === 403) { throw new VertexAuthError(`Authentication failed: ${errorMessage}`); } else if (statusCode === 400) { throw new VertexConfigError(`Invalid request: ${errorMessage}`); } else { throw new VertexApiError( `API error (${statusCode}): ${errorMessage}`, statusCode ); } } // Generic error handling throw new Error(`Vertex AI ${operation} failed: ${error.message}`); } } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/config/services/config-persistence.service.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview Configuration Persistence Service * Handles saving and backup of configuration files */ import { promises as fs } from 'node:fs'; import path from 'node:path'; import type { PartialConfiguration } from '../../interfaces/configuration.interface.js'; import { ERROR_CODES, TaskMasterError } from '../../errors/task-master-error.js'; /** * Persistence options */ export interface PersistenceOptions { /** Enable backup before saving */ createBackup?: boolean; /** Maximum number of backups to keep */ maxBackups?: number; /** Use atomic write operations */ atomic?: boolean; } /** * ConfigPersistence handles all configuration file I/O operations * Single responsibility: Configuration persistence */ export class ConfigPersistence { private localConfigPath: string; private backupDir: string; constructor(projectRoot: string) { this.localConfigPath = path.join(projectRoot, '.taskmaster', 'config.json'); this.backupDir = path.join(projectRoot, '.taskmaster', 'backups'); } /** * Save configuration to file */ async saveConfig( config: PartialConfiguration, options: PersistenceOptions = {} ): Promise<void> { const { createBackup = false, atomic = true } = options; try { // Create backup if requested if (createBackup && (await this.configExists())) { await this.createBackup(); } // Ensure directory exists const configDir = path.dirname(this.localConfigPath); await fs.mkdir(configDir, { recursive: true }); const jsonContent = JSON.stringify(config, null, 2); if (atomic) { // Atomic write: write to temp file then rename const tempPath = `${this.localConfigPath}.tmp`; await fs.writeFile(tempPath, jsonContent, 'utf-8'); await fs.rename(tempPath, this.localConfigPath); } else { // Direct write await fs.writeFile(this.localConfigPath, jsonContent, 'utf-8'); } } catch (error) { throw new TaskMasterError( 'Failed to save configuration', ERROR_CODES.CONFIG_ERROR, { configPath: this.localConfigPath }, error as Error ); } } /** * Create a backup of the current configuration */ private async createBackup(): Promise<string> { try { await fs.mkdir(this.backupDir, { recursive: true }); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = path.join(this.backupDir, `config-${timestamp}.json`); const configContent = await fs.readFile(this.localConfigPath, 'utf-8'); await fs.writeFile(backupPath, configContent, 'utf-8'); // Clean old backups await this.cleanOldBackups(); return backupPath; } catch (error) { console.warn('Failed to create backup:', error); throw error; } } /** * Clean old backup files */ private async cleanOldBackups(maxBackups = 5): Promise<void> { try { const files = await fs.readdir(this.backupDir); const backupFiles = files .filter((f) => f.startsWith('config-') && f.endsWith('.json')) .sort() .reverse(); // Remove old backups const toDelete = backupFiles.slice(maxBackups); for (const file of toDelete) { await fs.unlink(path.join(this.backupDir, file)); } } catch (error) { console.warn('Failed to clean old backups:', error); } } /** * Check if config file exists */ async configExists(): Promise<boolean> { try { await fs.access(this.localConfigPath); return true; } catch { return false; } } /** * Delete configuration file */ async deleteConfig(): Promise<void> { try { await fs.unlink(this.localConfigPath); } catch (error: any) { if (error.code !== 'ENOENT') { throw new TaskMasterError( 'Failed to delete configuration', ERROR_CODES.CONFIG_ERROR, { configPath: this.localConfigPath }, error ); } } } /** * Get list of available backups */ async getBackups(): Promise<string[]> { try { const files = await fs.readdir(this.backupDir); return files .filter((f) => f.startsWith('config-') && f.endsWith('.json')) .sort() .reverse(); } catch { return []; } } /** * Restore from a backup */ async restoreFromBackup(backupFile: string): Promise<void> { const backupPath = path.join(this.backupDir, backupFile); try { const backupContent = await fs.readFile(backupPath, 'utf-8'); await fs.writeFile(this.localConfigPath, backupContent, 'utf-8'); } catch (error) { throw new TaskMasterError( 'Failed to restore from backup', ERROR_CODES.CONFIG_ERROR, { backupPath }, error as Error ); } } } ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/update-single-task-status.test.js: -------------------------------------------------------------------------------- ```javascript /** * Tests for the updateSingleTaskStatus function */ import { jest } from '@jest/globals'; // Import test fixtures import { isValidTaskStatus, TASK_STATUS_OPTIONS } from '../../../../../src/constants/task-status.js'; // Sample tasks data for testing const sampleTasks = { tasks: [ { id: 1, title: 'Task 1', description: 'First task', status: 'pending', dependencies: [] }, { id: 2, title: 'Task 2', description: 'Second task', status: 'pending', dependencies: [] }, { id: 3, title: 'Task 3', description: 'Third task with subtasks', status: 'pending', dependencies: [], subtasks: [ { id: 1, title: 'Subtask 3.1', description: 'First subtask', status: 'pending', dependencies: [] }, { id: 2, title: 'Subtask 3.2', description: 'Second subtask', status: 'pending', dependencies: [] } ] } ] }; // Simplified version of updateSingleTaskStatus for testing const testUpdateSingleTaskStatus = (tasksData, taskIdInput, newStatus) => { 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 = tasksData.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 subtask.status = 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' ); // For testing, we don't need to output suggestions } } else { // Handle regular task const taskId = parseInt(taskIdInput, 10); const task = tasksData.tasks.find((t) => t.id === taskId); if (!task) { throw new Error(`Task ${taskId} not found`); } // Update the task status task.status = newStatus; // If marking as done, also mark all subtasks as done if ( (newStatus.toLowerCase() === 'done' || newStatus.toLowerCase() === 'completed') && task.subtasks && task.subtasks.length > 0 ) { task.subtasks.forEach((subtask) => { subtask.status = newStatus; }); } } return true; }; describe('updateSingleTaskStatus function', () => { test('should update regular task status', async () => { // Arrange const testTasksData = JSON.parse(JSON.stringify(sampleTasks)); // Act const result = testUpdateSingleTaskStatus(testTasksData, '2', 'done'); // Assert expect(result).toBe(true); expect(testTasksData.tasks[1].status).toBe('done'); }); test('should throw error for invalid status', async () => { // Arrange const testTasksData = JSON.parse(JSON.stringify(sampleTasks)); // Assert expect(() => testUpdateSingleTaskStatus(testTasksData, '2', 'Done') ).toThrow(/Error: Invalid status value: Done./); }); test('should update subtask status', async () => { // Arrange const testTasksData = JSON.parse(JSON.stringify(sampleTasks)); // Act const result = testUpdateSingleTaskStatus(testTasksData, '3.1', 'done'); // Assert expect(result).toBe(true); expect(testTasksData.tasks[2].subtasks[0].status).toBe('done'); }); test('should handle parent tasks without subtasks', async () => { // Arrange const testTasksData = JSON.parse(JSON.stringify(sampleTasks)); // Remove subtasks from task 3 const taskWithoutSubtasks = { ...testTasksData.tasks[2] }; delete taskWithoutSubtasks.subtasks; testTasksData.tasks[2] = taskWithoutSubtasks; // Assert expect(() => testUpdateSingleTaskStatus(testTasksData, '3.1', 'done') ).toThrow('has no subtasks'); }); test('should handle non-existent subtask ID', async () => { // Arrange const testTasksData = JSON.parse(JSON.stringify(sampleTasks)); // Assert expect(() => testUpdateSingleTaskStatus(testTasksData, '3.99', 'done') ).toThrow('Subtask 99 not found'); }); }); ```