This is page 40 of 52. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /context/fastmcp-core.txt: -------------------------------------------------------------------------------- ``` 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { 4 | CallToolRequestSchema, 5 | ClientCapabilities, 6 | CompleteRequestSchema, 7 | CreateMessageRequestSchema, 8 | ErrorCode, 9 | GetPromptRequestSchema, 10 | ListPromptsRequestSchema, 11 | ListResourcesRequestSchema, 12 | ListResourceTemplatesRequestSchema, 13 | ListToolsRequestSchema, 14 | McpError, 15 | ReadResourceRequestSchema, 16 | Root, 17 | RootsListChangedNotificationSchema, 18 | ServerCapabilities, 19 | SetLevelRequestSchema, 20 | } from "@modelcontextprotocol/sdk/types.js"; 21 | import { zodToJsonSchema } from "zod-to-json-schema"; 22 | import { z } from "zod"; 23 | import { setTimeout as delay } from "timers/promises"; 24 | import { readFile } from "fs/promises"; 25 | import { fileTypeFromBuffer } from "file-type"; 26 | import { StrictEventEmitter } from "strict-event-emitter-types"; 27 | import { EventEmitter } from "events"; 28 | import Fuse from "fuse.js"; 29 | import { startSSEServer } from "mcp-proxy"; 30 | import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; 31 | import parseURITemplate from "uri-templates"; 32 | import http from "http"; 33 | import { 34 | fetch 35 | } from "undici"; 36 | 37 | export type SSEServer = { 38 | close: () => Promise<void>; 39 | }; 40 | 41 | type FastMCPEvents<T extends FastMCPSessionAuth> = { 42 | connect: (event: { session: FastMCPSession<T> }) => void; 43 | disconnect: (event: { session: FastMCPSession<T> }) => void; 44 | }; 45 | 46 | type FastMCPSessionEvents = { 47 | rootsChanged: (event: { roots: Root[] }) => void; 48 | error: (event: { error: Error }) => void; 49 | }; 50 | 51 | /** 52 | * Generates an image content object from a URL, file path, or buffer. 53 | */ 54 | export const imageContent = async ( 55 | input: { url: string } | { path: string } | { buffer: Buffer }, 56 | ): Promise<ImageContent> => { 57 | let rawData: Buffer; 58 | 59 | if ("url" in input) { 60 | const response = await fetch(input.url); 61 | 62 | if (!response.ok) { 63 | throw new Error(`Failed to fetch image from URL: ${response.statusText}`); 64 | } 65 | 66 | rawData = Buffer.from(await response.arrayBuffer()); 67 | } else if ("path" in input) { 68 | rawData = await readFile(input.path); 69 | } else if ("buffer" in input) { 70 | rawData = input.buffer; 71 | } else { 72 | throw new Error( 73 | "Invalid input: Provide a valid 'url', 'path', or 'buffer'", 74 | ); 75 | } 76 | 77 | const mimeType = await fileTypeFromBuffer(rawData); 78 | 79 | const base64Data = rawData.toString("base64"); 80 | 81 | return { 82 | type: "image", 83 | data: base64Data, 84 | mimeType: mimeType?.mime ?? "image/png", 85 | } as const; 86 | }; 87 | 88 | abstract class FastMCPError extends Error { 89 | public constructor(message?: string) { 90 | super(message); 91 | this.name = new.target.name; 92 | } 93 | } 94 | 95 | type Extra = unknown; 96 | 97 | type Extras = Record<string, Extra>; 98 | 99 | export class UnexpectedStateError extends FastMCPError { 100 | public extras?: Extras; 101 | 102 | public constructor(message: string, extras?: Extras) { 103 | super(message); 104 | this.name = new.target.name; 105 | this.extras = extras; 106 | } 107 | } 108 | 109 | /** 110 | * An error that is meant to be surfaced to the user. 111 | */ 112 | export class UserError extends UnexpectedStateError {} 113 | 114 | type ToolParameters = z.ZodTypeAny; 115 | 116 | type Literal = boolean | null | number | string | undefined; 117 | 118 | type SerializableValue = 119 | | Literal 120 | | SerializableValue[] 121 | | { [key: string]: SerializableValue }; 122 | 123 | type Progress = { 124 | /** 125 | * The progress thus far. This should increase every time progress is made, even if the total is unknown. 126 | */ 127 | progress: number; 128 | /** 129 | * Total number of items to process (or total progress required), if known. 130 | */ 131 | total?: number; 132 | }; 133 | 134 | type Context<T extends FastMCPSessionAuth> = { 135 | session: T | undefined; 136 | reportProgress: (progress: Progress) => Promise<void>; 137 | log: { 138 | debug: (message: string, data?: SerializableValue) => void; 139 | error: (message: string, data?: SerializableValue) => void; 140 | info: (message: string, data?: SerializableValue) => void; 141 | warn: (message: string, data?: SerializableValue) => void; 142 | }; 143 | }; 144 | 145 | type TextContent = { 146 | type: "text"; 147 | text: string; 148 | }; 149 | 150 | const TextContentZodSchema = z 151 | .object({ 152 | type: z.literal("text"), 153 | /** 154 | * The text content of the message. 155 | */ 156 | text: z.string(), 157 | }) 158 | .strict() satisfies z.ZodType<TextContent>; 159 | 160 | type ImageContent = { 161 | type: "image"; 162 | data: string; 163 | mimeType: string; 164 | }; 165 | 166 | const ImageContentZodSchema = z 167 | .object({ 168 | type: z.literal("image"), 169 | /** 170 | * The base64-encoded image data. 171 | */ 172 | data: z.string().base64(), 173 | /** 174 | * The MIME type of the image. Different providers may support different image types. 175 | */ 176 | mimeType: z.string(), 177 | }) 178 | .strict() satisfies z.ZodType<ImageContent>; 179 | 180 | type Content = TextContent | ImageContent; 181 | 182 | const ContentZodSchema = z.discriminatedUnion("type", [ 183 | TextContentZodSchema, 184 | ImageContentZodSchema, 185 | ]) satisfies z.ZodType<Content>; 186 | 187 | type ContentResult = { 188 | content: Content[]; 189 | isError?: boolean; 190 | }; 191 | 192 | const ContentResultZodSchema = z 193 | .object({ 194 | content: ContentZodSchema.array(), 195 | isError: z.boolean().optional(), 196 | }) 197 | .strict() satisfies z.ZodType<ContentResult>; 198 | 199 | type Completion = { 200 | values: string[]; 201 | total?: number; 202 | hasMore?: boolean; 203 | }; 204 | 205 | /** 206 | * https://github.com/modelcontextprotocol/typescript-sdk/blob/3164da64d085ec4e022ae881329eee7b72f208d4/src/types.ts#L983-L1003 207 | */ 208 | const CompletionZodSchema = z.object({ 209 | /** 210 | * An array of completion values. Must not exceed 100 items. 211 | */ 212 | values: z.array(z.string()).max(100), 213 | /** 214 | * The total number of completion options available. This can exceed the number of values actually sent in the response. 215 | */ 216 | total: z.optional(z.number().int()), 217 | /** 218 | * Indicates whether there are additional completion options beyond those provided in the current response, even if the exact total is unknown. 219 | */ 220 | hasMore: z.optional(z.boolean()), 221 | }) satisfies z.ZodType<Completion>; 222 | 223 | type Tool<T extends FastMCPSessionAuth, Params extends ToolParameters = ToolParameters> = { 224 | name: string; 225 | description?: string; 226 | parameters?: Params; 227 | execute: ( 228 | args: z.infer<Params>, 229 | context: Context<T>, 230 | ) => Promise<string | ContentResult | TextContent | ImageContent>; 231 | }; 232 | 233 | type ResourceResult = 234 | | { 235 | text: string; 236 | } 237 | | { 238 | blob: string; 239 | }; 240 | 241 | type InputResourceTemplateArgument = Readonly<{ 242 | name: string; 243 | description?: string; 244 | complete?: ArgumentValueCompleter; 245 | }>; 246 | 247 | type ResourceTemplateArgument = Readonly<{ 248 | name: string; 249 | description?: string; 250 | complete?: ArgumentValueCompleter; 251 | }>; 252 | 253 | type ResourceTemplate< 254 | Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[], 255 | > = { 256 | uriTemplate: string; 257 | name: string; 258 | description?: string; 259 | mimeType?: string; 260 | arguments: Arguments; 261 | complete?: (name: string, value: string) => Promise<Completion>; 262 | load: ( 263 | args: ResourceTemplateArgumentsToObject<Arguments>, 264 | ) => Promise<ResourceResult>; 265 | }; 266 | 267 | type ResourceTemplateArgumentsToObject<T extends { name: string }[]> = { 268 | [K in T[number]["name"]]: string; 269 | }; 270 | 271 | type InputResourceTemplate< 272 | Arguments extends ResourceTemplateArgument[] = ResourceTemplateArgument[], 273 | > = { 274 | uriTemplate: string; 275 | name: string; 276 | description?: string; 277 | mimeType?: string; 278 | arguments: Arguments; 279 | load: ( 280 | args: ResourceTemplateArgumentsToObject<Arguments>, 281 | ) => Promise<ResourceResult>; 282 | }; 283 | 284 | type Resource = { 285 | uri: string; 286 | name: string; 287 | description?: string; 288 | mimeType?: string; 289 | load: () => Promise<ResourceResult | ResourceResult[]>; 290 | complete?: (name: string, value: string) => Promise<Completion>; 291 | }; 292 | 293 | type ArgumentValueCompleter = (value: string) => Promise<Completion>; 294 | 295 | type InputPromptArgument = Readonly<{ 296 | name: string; 297 | description?: string; 298 | required?: boolean; 299 | complete?: ArgumentValueCompleter; 300 | enum?: string[]; 301 | }>; 302 | 303 | type PromptArgumentsToObject<T extends { name: string; required?: boolean }[]> = 304 | { 305 | [K in T[number]["name"]]: Extract< 306 | T[number], 307 | { name: K } 308 | >["required"] extends true 309 | ? string 310 | : string | undefined; 311 | }; 312 | 313 | type InputPrompt< 314 | Arguments extends InputPromptArgument[] = InputPromptArgument[], 315 | Args = PromptArgumentsToObject<Arguments>, 316 | > = { 317 | name: string; 318 | description?: string; 319 | arguments?: InputPromptArgument[]; 320 | load: (args: Args) => Promise<string>; 321 | }; 322 | 323 | type PromptArgument = Readonly<{ 324 | name: string; 325 | description?: string; 326 | required?: boolean; 327 | complete?: ArgumentValueCompleter; 328 | enum?: string[]; 329 | }>; 330 | 331 | type Prompt< 332 | Arguments extends PromptArgument[] = PromptArgument[], 333 | Args = PromptArgumentsToObject<Arguments>, 334 | > = { 335 | arguments?: PromptArgument[]; 336 | complete?: (name: string, value: string) => Promise<Completion>; 337 | description?: string; 338 | load: (args: Args) => Promise<string>; 339 | name: string; 340 | }; 341 | 342 | type ServerOptions<T extends FastMCPSessionAuth> = { 343 | name: string; 344 | version: `${number}.${number}.${number}`; 345 | authenticate?: Authenticate<T>; 346 | }; 347 | 348 | type LoggingLevel = 349 | | "debug" 350 | | "info" 351 | | "notice" 352 | | "warning" 353 | | "error" 354 | | "critical" 355 | | "alert" 356 | | "emergency"; 357 | 358 | const FastMCPSessionEventEmitterBase: { 359 | new (): StrictEventEmitter<EventEmitter, FastMCPSessionEvents>; 360 | } = EventEmitter; 361 | 362 | class FastMCPSessionEventEmitter extends FastMCPSessionEventEmitterBase {} 363 | 364 | type SamplingResponse = { 365 | model: string; 366 | stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string; 367 | role: "user" | "assistant"; 368 | content: TextContent | ImageContent; 369 | }; 370 | 371 | type FastMCPSessionAuth = Record<string, unknown> | undefined; 372 | 373 | export class FastMCPSession<T extends FastMCPSessionAuth = FastMCPSessionAuth> extends FastMCPSessionEventEmitter { 374 | #capabilities: ServerCapabilities = {}; 375 | #clientCapabilities?: ClientCapabilities; 376 | #loggingLevel: LoggingLevel = "info"; 377 | #prompts: Prompt[] = []; 378 | #resources: Resource[] = []; 379 | #resourceTemplates: ResourceTemplate[] = []; 380 | #roots: Root[] = []; 381 | #server: Server; 382 | #auth: T | undefined; 383 | 384 | constructor({ 385 | auth, 386 | name, 387 | version, 388 | tools, 389 | resources, 390 | resourcesTemplates, 391 | prompts, 392 | }: { 393 | auth?: T; 394 | name: string; 395 | version: string; 396 | tools: Tool<T>[]; 397 | resources: Resource[]; 398 | resourcesTemplates: InputResourceTemplate[]; 399 | prompts: Prompt[]; 400 | }) { 401 | super(); 402 | 403 | this.#auth = auth; 404 | 405 | if (tools.length) { 406 | this.#capabilities.tools = {}; 407 | } 408 | 409 | if (resources.length || resourcesTemplates.length) { 410 | this.#capabilities.resources = {}; 411 | } 412 | 413 | if (prompts.length) { 414 | for (const prompt of prompts) { 415 | this.addPrompt(prompt); 416 | } 417 | 418 | this.#capabilities.prompts = {}; 419 | } 420 | 421 | this.#capabilities.logging = {}; 422 | 423 | this.#server = new Server( 424 | { name: name, version: version }, 425 | { capabilities: this.#capabilities }, 426 | ); 427 | 428 | this.setupErrorHandling(); 429 | this.setupLoggingHandlers(); 430 | this.setupRootsHandlers(); 431 | this.setupCompleteHandlers(); 432 | 433 | if (tools.length) { 434 | this.setupToolHandlers(tools); 435 | } 436 | 437 | if (resources.length || resourcesTemplates.length) { 438 | for (const resource of resources) { 439 | this.addResource(resource); 440 | } 441 | 442 | this.setupResourceHandlers(resources); 443 | 444 | if (resourcesTemplates.length) { 445 | for (const resourceTemplate of resourcesTemplates) { 446 | this.addResourceTemplate(resourceTemplate); 447 | } 448 | 449 | this.setupResourceTemplateHandlers(resourcesTemplates); 450 | } 451 | } 452 | 453 | if (prompts.length) { 454 | this.setupPromptHandlers(prompts); 455 | } 456 | } 457 | 458 | private addResource(inputResource: Resource) { 459 | this.#resources.push(inputResource); 460 | } 461 | 462 | private addResourceTemplate(inputResourceTemplate: InputResourceTemplate) { 463 | const completers: Record<string, ArgumentValueCompleter> = {}; 464 | 465 | for (const argument of inputResourceTemplate.arguments ?? []) { 466 | if (argument.complete) { 467 | completers[argument.name] = argument.complete; 468 | } 469 | } 470 | 471 | const resourceTemplate = { 472 | ...inputResourceTemplate, 473 | complete: async (name: string, value: string) => { 474 | if (completers[name]) { 475 | return await completers[name](value); 476 | } 477 | 478 | return { 479 | values: [], 480 | }; 481 | }, 482 | }; 483 | 484 | this.#resourceTemplates.push(resourceTemplate); 485 | } 486 | 487 | private addPrompt(inputPrompt: InputPrompt) { 488 | const completers: Record<string, ArgumentValueCompleter> = {}; 489 | const enums: Record<string, string[]> = {}; 490 | 491 | for (const argument of inputPrompt.arguments ?? []) { 492 | if (argument.complete) { 493 | completers[argument.name] = argument.complete; 494 | } 495 | 496 | if (argument.enum) { 497 | enums[argument.name] = argument.enum; 498 | } 499 | } 500 | 501 | const prompt = { 502 | ...inputPrompt, 503 | complete: async (name: string, value: string) => { 504 | if (completers[name]) { 505 | return await completers[name](value); 506 | } 507 | 508 | if (enums[name]) { 509 | const fuse = new Fuse(enums[name], { 510 | keys: ["value"], 511 | }); 512 | 513 | const result = fuse.search(value); 514 | 515 | return { 516 | values: result.map((item) => item.item), 517 | total: result.length, 518 | }; 519 | } 520 | 521 | return { 522 | values: [], 523 | }; 524 | }, 525 | }; 526 | 527 | this.#prompts.push(prompt); 528 | } 529 | 530 | public get clientCapabilities(): ClientCapabilities | null { 531 | return this.#clientCapabilities ?? null; 532 | } 533 | 534 | public get server(): Server { 535 | return this.#server; 536 | } 537 | 538 | #pingInterval: ReturnType<typeof setInterval> | null = null; 539 | 540 | public async requestSampling( 541 | message: z.infer<typeof CreateMessageRequestSchema>["params"], 542 | ): Promise<SamplingResponse> { 543 | return this.#server.createMessage(message); 544 | } 545 | 546 | public async connect(transport: Transport) { 547 | if (this.#server.transport) { 548 | throw new UnexpectedStateError("Server is already connected"); 549 | } 550 | 551 | await this.#server.connect(transport); 552 | 553 | let attempt = 0; 554 | 555 | while (attempt++ < 10) { 556 | const capabilities = await this.#server.getClientCapabilities(); 557 | 558 | if (capabilities) { 559 | this.#clientCapabilities = capabilities; 560 | 561 | break; 562 | } 563 | 564 | await delay(100); 565 | } 566 | 567 | if (!this.#clientCapabilities) { 568 | console.warn('[warning] FastMCP could not infer client capabilities') 569 | } 570 | 571 | if (this.#clientCapabilities?.roots?.listChanged) { 572 | try { 573 | const roots = await this.#server.listRoots(); 574 | this.#roots = roots.roots; 575 | } catch(e) { 576 | console.error(`[error] FastMCP received error listing roots.\n\n${e instanceof Error ? e.stack : JSON.stringify(e)}`) 577 | } 578 | } 579 | 580 | this.#pingInterval = setInterval(async () => { 581 | try { 582 | await this.#server.ping(); 583 | } catch (error) { 584 | this.emit("error", { 585 | error: error as Error, 586 | }); 587 | } 588 | }, 1000); 589 | } 590 | 591 | public get roots(): Root[] { 592 | return this.#roots; 593 | } 594 | 595 | public async close() { 596 | if (this.#pingInterval) { 597 | clearInterval(this.#pingInterval); 598 | } 599 | 600 | try { 601 | await this.#server.close(); 602 | } catch (error) { 603 | console.error("[MCP Error]", "could not close server", error); 604 | } 605 | } 606 | 607 | private setupErrorHandling() { 608 | this.#server.onerror = (error) => { 609 | console.error("[MCP Error]", error); 610 | }; 611 | } 612 | 613 | public get loggingLevel(): LoggingLevel { 614 | return this.#loggingLevel; 615 | } 616 | 617 | private setupCompleteHandlers() { 618 | this.#server.setRequestHandler(CompleteRequestSchema, async (request) => { 619 | if (request.params.ref.type === "ref/prompt") { 620 | const prompt = this.#prompts.find( 621 | (prompt) => prompt.name === request.params.ref.name, 622 | ); 623 | 624 | if (!prompt) { 625 | throw new UnexpectedStateError("Unknown prompt", { 626 | request, 627 | }); 628 | } 629 | 630 | if (!prompt.complete) { 631 | throw new UnexpectedStateError("Prompt does not support completion", { 632 | request, 633 | }); 634 | } 635 | 636 | const completion = CompletionZodSchema.parse( 637 | await prompt.complete( 638 | request.params.argument.name, 639 | request.params.argument.value, 640 | ), 641 | ); 642 | 643 | return { 644 | completion, 645 | }; 646 | } 647 | 648 | if (request.params.ref.type === "ref/resource") { 649 | const resource = this.#resourceTemplates.find( 650 | (resource) => resource.uriTemplate === request.params.ref.uri, 651 | ); 652 | 653 | if (!resource) { 654 | throw new UnexpectedStateError("Unknown resource", { 655 | request, 656 | }); 657 | } 658 | 659 | if (!("uriTemplate" in resource)) { 660 | throw new UnexpectedStateError("Unexpected resource"); 661 | } 662 | 663 | if (!resource.complete) { 664 | throw new UnexpectedStateError( 665 | "Resource does not support completion", 666 | { 667 | request, 668 | }, 669 | ); 670 | } 671 | 672 | const completion = CompletionZodSchema.parse( 673 | await resource.complete( 674 | request.params.argument.name, 675 | request.params.argument.value, 676 | ), 677 | ); 678 | 679 | return { 680 | completion, 681 | }; 682 | } 683 | 684 | throw new UnexpectedStateError("Unexpected completion request", { 685 | request, 686 | }); 687 | }); 688 | } 689 | 690 | private setupRootsHandlers() { 691 | this.#server.setNotificationHandler( 692 | RootsListChangedNotificationSchema, 693 | () => { 694 | this.#server.listRoots().then((roots) => { 695 | this.#roots = roots.roots; 696 | 697 | this.emit("rootsChanged", { 698 | roots: roots.roots, 699 | }); 700 | }); 701 | }, 702 | ); 703 | } 704 | 705 | private setupLoggingHandlers() { 706 | this.#server.setRequestHandler(SetLevelRequestSchema, (request) => { 707 | this.#loggingLevel = request.params.level; 708 | 709 | return {}; 710 | }); 711 | } 712 | 713 | private setupToolHandlers(tools: Tool<T>[]) { 714 | this.#server.setRequestHandler(ListToolsRequestSchema, async () => { 715 | return { 716 | tools: tools.map((tool) => { 717 | return { 718 | name: tool.name, 719 | description: tool.description, 720 | inputSchema: tool.parameters 721 | ? zodToJsonSchema(tool.parameters) 722 | : undefined, 723 | }; 724 | }), 725 | }; 726 | }); 727 | 728 | this.#server.setRequestHandler(CallToolRequestSchema, async (request) => { 729 | const tool = tools.find((tool) => tool.name === request.params.name); 730 | 731 | if (!tool) { 732 | throw new McpError( 733 | ErrorCode.MethodNotFound, 734 | `Unknown tool: ${request.params.name}`, 735 | ); 736 | } 737 | 738 | let args: any = undefined; 739 | 740 | if (tool.parameters) { 741 | const parsed = tool.parameters.safeParse(request.params.arguments); 742 | 743 | if (!parsed.success) { 744 | throw new McpError( 745 | ErrorCode.InvalidParams, 746 | `Invalid ${request.params.name} parameters`, 747 | ); 748 | } 749 | 750 | args = parsed.data; 751 | } 752 | 753 | const progressToken = request.params?._meta?.progressToken; 754 | 755 | let result: ContentResult; 756 | 757 | try { 758 | const reportProgress = async (progress: Progress) => { 759 | await this.#server.notification({ 760 | method: "notifications/progress", 761 | params: { 762 | ...progress, 763 | progressToken, 764 | }, 765 | }); 766 | }; 767 | 768 | const log = { 769 | debug: (message: string, context?: SerializableValue) => { 770 | this.#server.sendLoggingMessage({ 771 | level: "debug", 772 | data: { 773 | message, 774 | context, 775 | }, 776 | }); 777 | }, 778 | error: (message: string, context?: SerializableValue) => { 779 | this.#server.sendLoggingMessage({ 780 | level: "error", 781 | data: { 782 | message, 783 | context, 784 | }, 785 | }); 786 | }, 787 | info: (message: string, context?: SerializableValue) => { 788 | this.#server.sendLoggingMessage({ 789 | level: "info", 790 | data: { 791 | message, 792 | context, 793 | }, 794 | }); 795 | }, 796 | warn: (message: string, context?: SerializableValue) => { 797 | this.#server.sendLoggingMessage({ 798 | level: "warning", 799 | data: { 800 | message, 801 | context, 802 | }, 803 | }); 804 | }, 805 | }; 806 | 807 | const maybeStringResult = await tool.execute(args, { 808 | reportProgress, 809 | log, 810 | session: this.#auth, 811 | }); 812 | 813 | if (typeof maybeStringResult === "string") { 814 | result = ContentResultZodSchema.parse({ 815 | content: [{ type: "text", text: maybeStringResult }], 816 | }); 817 | } else if ("type" in maybeStringResult) { 818 | result = ContentResultZodSchema.parse({ 819 | content: [maybeStringResult], 820 | }); 821 | } else { 822 | result = ContentResultZodSchema.parse(maybeStringResult); 823 | } 824 | } catch (error) { 825 | if (error instanceof UserError) { 826 | return { 827 | content: [{ type: "text", text: error.message }], 828 | isError: true, 829 | }; 830 | } 831 | 832 | return { 833 | content: [{ type: "text", text: `Error: ${error}` }], 834 | isError: true, 835 | }; 836 | } 837 | 838 | return result; 839 | }); 840 | } 841 | 842 | private setupResourceHandlers(resources: Resource[]) { 843 | this.#server.setRequestHandler(ListResourcesRequestSchema, async () => { 844 | return { 845 | resources: resources.map((resource) => { 846 | return { 847 | uri: resource.uri, 848 | name: resource.name, 849 | mimeType: resource.mimeType, 850 | }; 851 | }), 852 | }; 853 | }); 854 | 855 | this.#server.setRequestHandler( 856 | ReadResourceRequestSchema, 857 | async (request) => { 858 | if ("uri" in request.params) { 859 | const resource = resources.find( 860 | (resource) => 861 | "uri" in resource && resource.uri === request.params.uri, 862 | ); 863 | 864 | if (!resource) { 865 | for (const resourceTemplate of this.#resourceTemplates) { 866 | const uriTemplate = parseURITemplate( 867 | resourceTemplate.uriTemplate, 868 | ); 869 | 870 | const match = uriTemplate.fromUri(request.params.uri); 871 | 872 | if (!match) { 873 | continue; 874 | } 875 | 876 | const uri = uriTemplate.fill(match); 877 | 878 | const result = await resourceTemplate.load(match); 879 | 880 | return { 881 | contents: [ 882 | { 883 | uri: uri, 884 | mimeType: resourceTemplate.mimeType, 885 | name: resourceTemplate.name, 886 | ...result, 887 | }, 888 | ], 889 | }; 890 | } 891 | 892 | throw new McpError( 893 | ErrorCode.MethodNotFound, 894 | `Unknown resource: ${request.params.uri}`, 895 | ); 896 | } 897 | 898 | if (!("uri" in resource)) { 899 | throw new UnexpectedStateError("Resource does not support reading"); 900 | } 901 | 902 | let maybeArrayResult: Awaited<ReturnType<Resource["load"]>>; 903 | 904 | try { 905 | maybeArrayResult = await resource.load(); 906 | } catch (error) { 907 | throw new McpError( 908 | ErrorCode.InternalError, 909 | `Error reading resource: ${error}`, 910 | { 911 | uri: resource.uri, 912 | }, 913 | ); 914 | } 915 | 916 | if (Array.isArray(maybeArrayResult)) { 917 | return { 918 | contents: maybeArrayResult.map((result) => ({ 919 | uri: resource.uri, 920 | mimeType: resource.mimeType, 921 | name: resource.name, 922 | ...result, 923 | })), 924 | }; 925 | } else { 926 | return { 927 | contents: [ 928 | { 929 | uri: resource.uri, 930 | mimeType: resource.mimeType, 931 | name: resource.name, 932 | ...maybeArrayResult, 933 | }, 934 | ], 935 | }; 936 | } 937 | } 938 | 939 | throw new UnexpectedStateError("Unknown resource request", { 940 | request, 941 | }); 942 | }, 943 | ); 944 | } 945 | 946 | private setupResourceTemplateHandlers(resourceTemplates: ResourceTemplate[]) { 947 | this.#server.setRequestHandler( 948 | ListResourceTemplatesRequestSchema, 949 | async () => { 950 | return { 951 | resourceTemplates: resourceTemplates.map((resourceTemplate) => { 952 | return { 953 | name: resourceTemplate.name, 954 | uriTemplate: resourceTemplate.uriTemplate, 955 | }; 956 | }), 957 | }; 958 | }, 959 | ); 960 | } 961 | 962 | private setupPromptHandlers(prompts: Prompt[]) { 963 | this.#server.setRequestHandler(ListPromptsRequestSchema, async () => { 964 | return { 965 | prompts: prompts.map((prompt) => { 966 | return { 967 | name: prompt.name, 968 | description: prompt.description, 969 | arguments: prompt.arguments, 970 | complete: prompt.complete, 971 | }; 972 | }), 973 | }; 974 | }); 975 | 976 | this.#server.setRequestHandler(GetPromptRequestSchema, async (request) => { 977 | const prompt = prompts.find( 978 | (prompt) => prompt.name === request.params.name, 979 | ); 980 | 981 | if (!prompt) { 982 | throw new McpError( 983 | ErrorCode.MethodNotFound, 984 | `Unknown prompt: ${request.params.name}`, 985 | ); 986 | } 987 | 988 | const args = request.params.arguments; 989 | 990 | for (const arg of prompt.arguments ?? []) { 991 | if (arg.required && !(args && arg.name in args)) { 992 | throw new McpError( 993 | ErrorCode.InvalidRequest, 994 | `Missing required argument: ${arg.name}`, 995 | ); 996 | } 997 | } 998 | 999 | let result: Awaited<ReturnType<Prompt["load"]>>; 1000 | 1001 | try { 1002 | result = await prompt.load(args as Record<string, string | undefined>); 1003 | } catch (error) { 1004 | throw new McpError( 1005 | ErrorCode.InternalError, 1006 | `Error loading prompt: ${error}`, 1007 | ); 1008 | } 1009 | 1010 | return { 1011 | description: prompt.description, 1012 | messages: [ 1013 | { 1014 | role: "user", 1015 | content: { type: "text", text: result }, 1016 | }, 1017 | ], 1018 | }; 1019 | }); 1020 | } 1021 | } 1022 | 1023 | const FastMCPEventEmitterBase: { 1024 | new (): StrictEventEmitter<EventEmitter, FastMCPEvents<FastMCPSessionAuth>>; 1025 | } = EventEmitter; 1026 | 1027 | class FastMCPEventEmitter extends FastMCPEventEmitterBase {} 1028 | 1029 | type Authenticate<T> = (request: http.IncomingMessage) => Promise<T>; 1030 | 1031 | export class FastMCP<T extends Record<string, unknown> | undefined = undefined> extends FastMCPEventEmitter { 1032 | #options: ServerOptions<T>; 1033 | #prompts: InputPrompt[] = []; 1034 | #resources: Resource[] = []; 1035 | #resourcesTemplates: InputResourceTemplate[] = []; 1036 | #sessions: FastMCPSession<T>[] = []; 1037 | #sseServer: SSEServer | null = null; 1038 | #tools: Tool<T>[] = []; 1039 | #authenticate: Authenticate<T> | undefined; 1040 | 1041 | constructor(public options: ServerOptions<T>) { 1042 | super(); 1043 | 1044 | this.#options = options; 1045 | this.#authenticate = options.authenticate; 1046 | } 1047 | 1048 | public get sessions(): FastMCPSession<T>[] { 1049 | return this.#sessions; 1050 | } 1051 | 1052 | /** 1053 | * Adds a tool to the server. 1054 | */ 1055 | public addTool<Params extends ToolParameters>(tool: Tool<T, Params>) { 1056 | this.#tools.push(tool as unknown as Tool<T>); 1057 | } 1058 | 1059 | /** 1060 | * Adds a resource to the server. 1061 | */ 1062 | public addResource(resource: Resource) { 1063 | this.#resources.push(resource); 1064 | } 1065 | 1066 | /** 1067 | * Adds a resource template to the server. 1068 | */ 1069 | public addResourceTemplate< 1070 | const Args extends InputResourceTemplateArgument[], 1071 | >(resource: InputResourceTemplate<Args>) { 1072 | this.#resourcesTemplates.push(resource); 1073 | } 1074 | 1075 | /** 1076 | * Adds a prompt to the server. 1077 | */ 1078 | public addPrompt<const Args extends InputPromptArgument[]>( 1079 | prompt: InputPrompt<Args>, 1080 | ) { 1081 | this.#prompts.push(prompt); 1082 | } 1083 | 1084 | /** 1085 | * Starts the server. 1086 | */ 1087 | public async start( 1088 | options: 1089 | | { transportType: "stdio" } 1090 | | { 1091 | transportType: "sse"; 1092 | sse: { endpoint: `/${string}`; port: number }; 1093 | } = { 1094 | transportType: "stdio", 1095 | }, 1096 | ) { 1097 | if (options.transportType === "stdio") { 1098 | const transport = new StdioServerTransport(); 1099 | 1100 | const session = new FastMCPSession<T>({ 1101 | name: this.#options.name, 1102 | version: this.#options.version, 1103 | tools: this.#tools, 1104 | resources: this.#resources, 1105 | resourcesTemplates: this.#resourcesTemplates, 1106 | prompts: this.#prompts, 1107 | }); 1108 | 1109 | await session.connect(transport); 1110 | 1111 | this.#sessions.push(session); 1112 | 1113 | this.emit("connect", { 1114 | session, 1115 | }); 1116 | 1117 | } else if (options.transportType === "sse") { 1118 | this.#sseServer = await startSSEServer<FastMCPSession<T>>({ 1119 | endpoint: options.sse.endpoint as `/${string}`, 1120 | port: options.sse.port, 1121 | createServer: async (request) => { 1122 | let auth: T | undefined; 1123 | 1124 | if (this.#authenticate) { 1125 | auth = await this.#authenticate(request); 1126 | } 1127 | 1128 | return new FastMCPSession<T>({ 1129 | auth, 1130 | name: this.#options.name, 1131 | version: this.#options.version, 1132 | tools: this.#tools, 1133 | resources: this.#resources, 1134 | resourcesTemplates: this.#resourcesTemplates, 1135 | prompts: this.#prompts, 1136 | }); 1137 | }, 1138 | onClose: (session) => { 1139 | this.emit("disconnect", { 1140 | session, 1141 | }); 1142 | }, 1143 | onConnect: async (session) => { 1144 | this.#sessions.push(session); 1145 | 1146 | this.emit("connect", { 1147 | session, 1148 | }); 1149 | }, 1150 | }); 1151 | 1152 | console.info( 1153 | `server is running on SSE at http://localhost:${options.sse.port}${options.sse.endpoint}`, 1154 | ); 1155 | } else { 1156 | throw new Error("Invalid transport type"); 1157 | } 1158 | } 1159 | 1160 | /** 1161 | * Stops the server. 1162 | */ 1163 | public async stop() { 1164 | if (this.#sseServer) { 1165 | this.#sseServer.close(); 1166 | } 1167 | } 1168 | } 1169 | 1170 | export type { Context }; 1171 | export type { Tool, ToolParameters }; 1172 | export type { Content, TextContent, ImageContent, ContentResult }; 1173 | export type { Progress, SerializableValue }; 1174 | export type { Resource, ResourceResult }; 1175 | export type { ResourceTemplate, ResourceTemplateArgument }; 1176 | export type { Prompt, PromptArgument }; 1177 | export type { InputPrompt, InputPromptArgument }; 1178 | export type { ServerOptions, LoggingLevel }; 1179 | export type { FastMCPEvents, FastMCPSessionEvents }; ``` -------------------------------------------------------------------------------- /.kiro/steering/dev_workflow.md: -------------------------------------------------------------------------------- ```markdown 1 | --- 2 | inclusion: always 3 | --- 4 | 5 | # Taskmaster Development Workflow 6 | 7 | This guide outlines the standard process for using Taskmaster to manage software development projects. It is written as a set of instructions for you, the AI agent. 8 | 9 | - **Your Default Stance**: For most projects, the user can work directly within the `master` task context. Your initial actions should operate on this default context unless a clear pattern for multi-context work emerges. 10 | - **Your Goal**: Your role is to elevate the user's workflow by intelligently introducing advanced features like **Tagged Task Lists** when you detect the appropriate context. Do not force tags on the user; suggest them as a helpful solution to a specific need. 11 | 12 | ## The Basic Loop 13 | The fundamental development cycle you will facilitate is: 14 | 1. **`list`**: Show the user what needs to be done. 15 | 2. **`next`**: Help the user decide what to work on. 16 | 3. **`show <id>`**: Provide details for a specific task. 17 | 4. **`expand <id>`**: Break down a complex task into smaller, manageable subtasks. 18 | 5. **Implement**: The user writes the code and tests. 19 | 6. **`update-subtask`**: Log progress and findings on behalf of the user. 20 | 7. **`set-status`**: Mark tasks and subtasks as `done` as work is completed. 21 | 8. **Repeat**. 22 | 23 | All your standard command executions should operate on the user's current task context, which defaults to `master`. 24 | 25 | --- 26 | 27 | ## Standard Development Workflow Process 28 | 29 | ### Simple Workflow (Default Starting Point) 30 | 31 | For new projects or when users are getting started, operate within the `master` tag context: 32 | 33 | - Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input='<prd-file.txt>'` (see @`taskmaster.md`) to generate initial tasks.json with tagged structure 34 | - Configure rule sets during initialization with `--rules` flag (e.g., `task-master init --rules kiro,windsurf`) or manage them later with `task-master rules add/remove` commands 35 | - Begin coding sessions with `get_tasks` / `task-master list` (see @`taskmaster.md`) to see current tasks, status, and IDs 36 | - Determine the next task to work on using `next_task` / `task-master next` (see @`taskmaster.md`) 37 | - Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.md`) before breaking down tasks 38 | - Review complexity report using `complexity_report` / `task-master complexity-report` (see @`taskmaster.md`) 39 | - Select tasks based on dependencies (all marked 'done'), priority level, and ID order 40 | - View specific task details using `get_task` / `task-master show <id>` (see @`taskmaster.md`) to understand implementation requirements 41 | - Break down complex tasks using `expand_task` / `task-master expand --id=<id> --force --research` (see @`taskmaster.md`) with appropriate flags like `--force` (to replace existing subtasks) and `--research` 42 | - Implement code following task details, dependencies, and project standards 43 | - Mark completed tasks with `set_task_status` / `task-master set-status --id=<id> --status=done` (see @`taskmaster.md`) 44 | - Update dependent tasks when implementation differs from original plan using `update` / `task-master update --from=<id> --prompt="..."` or `update_task` / `task-master update-task --id=<id> --prompt="..."` (see @`taskmaster.md`) 45 | 46 | --- 47 | 48 | ## Leveling Up: Agent-Led Multi-Context Workflows 49 | 50 | While the basic workflow is powerful, your primary opportunity to add value is by identifying when to introduce **Tagged Task Lists**. These patterns are your tools for creating a more organized and efficient development environment for the user, especially if you detect agentic or parallel development happening across the same session. 51 | 52 | **Critical Principle**: Most users should never see a difference in their experience. Only introduce advanced workflows when you detect clear indicators that the project has evolved beyond simple task management. 53 | 54 | ### When to Introduce Tags: Your Decision Patterns 55 | 56 | Here are the patterns to look for. When you detect one, you should propose the corresponding workflow to the user. 57 | 58 | #### Pattern 1: Simple Git Feature Branching 59 | This is the most common and direct use case for tags. 60 | 61 | - **Trigger**: The user creates a new git branch (e.g., `git checkout -b feature/user-auth`). 62 | - **Your Action**: Propose creating a new tag that mirrors the branch name to isolate the feature's tasks from `master`. 63 | - **Your Suggested Prompt**: *"I see you've created a new branch named 'feature/user-auth'. To keep all related tasks neatly organized and separate from your main list, I can create a corresponding task tag for you. This helps prevent merge conflicts in your `tasks.json` file later. Shall I create the 'feature-user-auth' tag?"* 64 | - **Tool to Use**: `task-master add-tag --from-branch` 65 | 66 | #### Pattern 2: Team Collaboration 67 | - **Trigger**: The user mentions working with teammates (e.g., "My teammate Alice is handling the database schema," or "I need to review Bob's work on the API."). 68 | - **Your Action**: Suggest creating a separate tag for the user's work to prevent conflicts with shared master context. 69 | - **Your Suggested Prompt**: *"Since you're working with Alice, I can create a separate task context for your work to avoid conflicts. This way, Alice can continue working with the master list while you have your own isolated context. When you're ready to merge your work, we can coordinate the tasks back to master. Shall I create a tag for your current work?"* 70 | - **Tool to Use**: `task-master add-tag my-work --copy-from-current --description="My tasks while collaborating with Alice"` 71 | 72 | #### Pattern 3: Experiments or Risky Refactors 73 | - **Trigger**: The user wants to try something that might not be kept (e.g., "I want to experiment with switching our state management library," or "Let's refactor the old API module, but I want to keep the current tasks as a reference."). 74 | - **Your Action**: Propose creating a sandboxed tag for the experimental work. 75 | - **Your Suggested Prompt**: *"This sounds like a great experiment. To keep these new tasks separate from our main plan, I can create a temporary 'experiment-zustand' tag for this work. If we decide not to proceed, we can simply delete the tag without affecting the main task list. Sound good?"* 76 | - **Tool to Use**: `task-master add-tag experiment-zustand --description="Exploring Zustand migration"` 77 | 78 | #### Pattern 4: Large Feature Initiatives (PRD-Driven) 79 | This is a more structured approach for significant new features or epics. 80 | 81 | - **Trigger**: The user describes a large, multi-step feature that would benefit from a formal plan. 82 | - **Your Action**: Propose a comprehensive, PRD-driven workflow. 83 | - **Your Suggested Prompt**: *"This sounds like a significant new feature. To manage this effectively, I suggest we create a dedicated task context for it. Here's the plan: I'll create a new tag called 'feature-xyz', then we can draft a Product Requirements Document (PRD) together to scope the work. Once the PRD is ready, I'll automatically generate all the necessary tasks within that new tag. How does that sound?"* 84 | - **Your Implementation Flow**: 85 | 1. **Create an empty tag**: `task-master add-tag feature-xyz --description "Tasks for the new XYZ feature"`. You can also start by creating a git branch if applicable, and then create the tag from that branch. 86 | 2. **Collaborate & Create PRD**: Work with the user to create a detailed PRD file (e.g., `.taskmaster/docs/feature-xyz-prd.txt`). 87 | 3. **Parse PRD into the new tag**: `task-master parse-prd .taskmaster/docs/feature-xyz-prd.txt --tag feature-xyz` 88 | 4. **Prepare the new task list**: Follow up by suggesting `analyze-complexity` and `expand-all` for the newly created tasks within the `feature-xyz` tag. 89 | 90 | #### Pattern 5: Version-Based Development 91 | Tailor your approach based on the project maturity indicated by tag names. 92 | 93 | - **Prototype/MVP Tags** (`prototype`, `mvp`, `poc`, `v0.x`): 94 | - **Your Approach**: Focus on speed and functionality over perfection 95 | - **Task Generation**: Create tasks that emphasize "get it working" over "get it perfect" 96 | - **Complexity Level**: Lower complexity, fewer subtasks, more direct implementation paths 97 | - **Research Prompts**: Include context like "This is a prototype - prioritize speed and basic functionality over optimization" 98 | - **Example Prompt Addition**: *"Since this is for the MVP, I'll focus on tasks that get core functionality working quickly rather than over-engineering."* 99 | 100 | - **Production/Mature Tags** (`v1.0+`, `production`, `stable`): 101 | - **Your Approach**: Emphasize robustness, testing, and maintainability 102 | - **Task Generation**: Include comprehensive error handling, testing, documentation, and optimization 103 | - **Complexity Level**: Higher complexity, more detailed subtasks, thorough implementation paths 104 | - **Research Prompts**: Include context like "This is for production - prioritize reliability, performance, and maintainability" 105 | - **Example Prompt Addition**: *"Since this is for production, I'll ensure tasks include proper error handling, testing, and documentation."* 106 | 107 | ### Advanced Workflow (Tag-Based & PRD-Driven) 108 | 109 | **When to Transition**: Recognize when the project has evolved (or has initiated a project which existing code) beyond simple task management. Look for these indicators: 110 | - User mentions teammates or collaboration needs 111 | - Project has grown to 15+ tasks with mixed priorities 112 | - User creates feature branches or mentions major initiatives 113 | - User initializes Taskmaster on an existing, complex codebase 114 | - User describes large features that would benefit from dedicated planning 115 | 116 | **Your Role in Transition**: Guide the user to a more sophisticated workflow that leverages tags for organization and PRDs for comprehensive planning. 117 | 118 | #### Master List Strategy (High-Value Focus) 119 | Once you transition to tag-based workflows, the `master` tag should ideally contain only: 120 | - **High-level deliverables** that provide significant business value 121 | - **Major milestones** and epic-level features 122 | - **Critical infrastructure** work that affects the entire project 123 | - **Release-blocking** items 124 | 125 | **What NOT to put in master**: 126 | - Detailed implementation subtasks (these go in feature-specific tags' parent tasks) 127 | - Refactoring work (create dedicated tags like `refactor-auth`) 128 | - Experimental features (use `experiment-*` tags) 129 | - Team member-specific tasks (use person-specific tags) 130 | 131 | #### PRD-Driven Feature Development 132 | 133 | **For New Major Features**: 134 | 1. **Identify the Initiative**: When user describes a significant feature 135 | 2. **Create Dedicated Tag**: `add_tag feature-[name] --description="[Feature description]"` 136 | 3. **Collaborative PRD Creation**: Work with user to create comprehensive PRD in `.taskmaster/docs/feature-[name]-prd.txt` 137 | 4. **Parse & Prepare**: 138 | - `parse_prd .taskmaster/docs/feature-[name]-prd.txt --tag=feature-[name]` 139 | - `analyze_project_complexity --tag=feature-[name] --research` 140 | - `expand_all --tag=feature-[name] --research` 141 | 5. **Add Master Reference**: Create a high-level task in `master` that references the feature tag 142 | 143 | **For Existing Codebase Analysis**: 144 | When users initialize Taskmaster on existing projects: 145 | 1. **Codebase Discovery**: Use your native tools for producing deep context about the code base. You may use `research` tool with `--tree` and `--files` to collect up to date information using the existing architecture as context. 146 | 2. **Collaborative Assessment**: Work with user to identify improvement areas, technical debt, or new features 147 | 3. **Strategic PRD Creation**: Co-author PRDs that include: 148 | - Current state analysis (based on your codebase research) 149 | - Proposed improvements or new features 150 | - Implementation strategy considering existing code 151 | 4. **Tag-Based Organization**: Parse PRDs into appropriate tags (`refactor-api`, `feature-dashboard`, `tech-debt`, etc.) 152 | 5. **Master List Curation**: Keep only the most valuable initiatives in master 153 | 154 | The parse-prd's `--append` flag enables the user to parse multiple PRDs within tags or across tags. PRDs should be focused and the number of tasks they are parsed into should be strategically chosen relative to the PRD's complexity and level of detail. 155 | 156 | ### Workflow Transition Examples 157 | 158 | **Example 1: Simple → Team-Based** 159 | ``` 160 | User: "Alice is going to help with the API work" 161 | Your Response: "Great! To avoid conflicts, I'll create a separate task context for your work. Alice can continue with the master list while you work in your own context. When you're ready to merge, we can coordinate the tasks back together." 162 | Action: add_tag my-api-work --copy-from-current --description="My API tasks while collaborating with Alice" 163 | ``` 164 | 165 | **Example 2: Simple → PRD-Driven** 166 | ``` 167 | User: "I want to add a complete user dashboard with analytics, user management, and reporting" 168 | Your Response: "This sounds like a major feature that would benefit from detailed planning. Let me create a dedicated context for this work and we can draft a PRD together to ensure we capture all requirements." 169 | Actions: 170 | 1. add_tag feature-dashboard --description="User dashboard with analytics and management" 171 | 2. Collaborate on PRD creation 172 | 3. parse_prd dashboard-prd.txt --tag=feature-dashboard 173 | 4. Add high-level "User Dashboard" task to master 174 | ``` 175 | 176 | **Example 3: Existing Project → Strategic Planning** 177 | ``` 178 | User: "I just initialized Taskmaster on my existing React app. It's getting messy and I want to improve it." 179 | Your Response: "Let me research your codebase to understand the current architecture, then we can create a strategic plan for improvements." 180 | Actions: 181 | 1. research "Current React app architecture and improvement opportunities" --tree --files=src/ 182 | 2. Collaborate on improvement PRD based on findings 183 | 3. Create tags for different improvement areas (refactor-components, improve-state-management, etc.) 184 | 4. Keep only major improvement initiatives in master 185 | ``` 186 | 187 | --- 188 | 189 | ## Primary Interaction: MCP Server vs. CLI 190 | 191 | Taskmaster offers two primary ways to interact: 192 | 193 | 1. **MCP Server (Recommended for Integrated Tools)**: 194 | - For AI agents and integrated development environments (like Kiro), interacting via the **MCP server is the preferred method**. 195 | - The MCP server exposes Taskmaster functionality through a set of tools (e.g., `get_tasks`, `add_subtask`). 196 | - This method offers better performance, structured data exchange, and richer error handling compared to CLI parsing. 197 | - Refer to @`mcp.md` for details on the MCP architecture and available tools. 198 | - A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in @`taskmaster.md`. 199 | - **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change. 200 | - **Note**: MCP tools fully support tagged task lists with complete tag management capabilities. 201 | 202 | 2. **`task-master` CLI (For Users & Fallback)**: 203 | - The global `task-master` command provides a user-friendly interface for direct terminal interaction. 204 | - It can also serve as a fallback if the MCP server is inaccessible or a specific function isn't exposed via MCP. 205 | - Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`. 206 | - The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`). 207 | - Refer to @`taskmaster.md` for a detailed command reference. 208 | - **Tagged Task Lists**: CLI fully supports the new tagged system with seamless migration. 209 | 210 | ## How the Tag System Works (For Your Reference) 211 | 212 | - **Data Structure**: Tasks are organized into separate contexts (tags) like "master", "feature-branch", or "v2.0". 213 | - **Silent Migration**: Existing projects automatically migrate to use a "master" tag with zero disruption. 214 | - **Context Isolation**: Tasks in different tags are completely separate. Changes in one tag do not affect any other tag. 215 | - **Manual Control**: The user is always in control. There is no automatic switching. You facilitate switching by using `use-tag <name>`. 216 | - **Full CLI & MCP Support**: All tag management commands are available through both the CLI and MCP tools for you to use. Refer to @`taskmaster.md` for a full command list. 217 | 218 | --- 219 | 220 | ## Task Complexity Analysis 221 | 222 | - Run `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.md`) for comprehensive analysis 223 | - Review complexity report via `complexity_report` / `task-master complexity-report` (see @`taskmaster.md`) for a formatted, readable version. 224 | - Focus on tasks with highest complexity scores (8-10) for detailed breakdown 225 | - Use analysis results to determine appropriate subtask allocation 226 | - Note that reports are automatically used by the `expand_task` tool/command 227 | 228 | ## Task Breakdown Process 229 | 230 | - Use `expand_task` / `task-master expand --id=<id>`. It automatically uses the complexity report if found, otherwise generates default number of subtasks. 231 | - Use `--num=<number>` to specify an explicit number of subtasks, overriding defaults or complexity report recommendations. 232 | - Add `--research` flag to leverage Perplexity AI for research-backed expansion. 233 | - Add `--force` flag to clear existing subtasks before generating new ones (default is to append). 234 | - Use `--prompt="<context>"` to provide additional context when needed. 235 | - Review and adjust generated subtasks as necessary. 236 | - Use `expand_all` tool or `task-master expand --all` to expand multiple pending tasks at once, respecting flags like `--force` and `--research`. 237 | - If subtasks need complete replacement (regardless of the `--force` flag on `expand`), clear them first with `clear_subtasks` / `task-master clear-subtasks --id=<id>`. 238 | 239 | ## Implementation Drift Handling 240 | 241 | - When implementation differs significantly from planned approach 242 | - When future tasks need modification due to current implementation choices 243 | - When new dependencies or requirements emerge 244 | - Use `update` / `task-master update --from=<futureTaskId> --prompt='<explanation>\nUpdate context...' --research` to update multiple future tasks. 245 | - Use `update_task` / `task-master update-task --id=<taskId> --prompt='<explanation>\nUpdate context...' --research` to update a single specific task. 246 | 247 | ## Task Status Management 248 | 249 | - Use 'pending' for tasks ready to be worked on 250 | - Use 'done' for completed and verified tasks 251 | - Use 'deferred' for postponed tasks 252 | - Add custom status values as needed for project-specific workflows 253 | 254 | ## Task Structure Fields 255 | 256 | - **id**: Unique identifier for the task (Example: `1`, `1.1`) 257 | - **title**: Brief, descriptive title (Example: `"Initialize Repo"`) 258 | - **description**: Concise summary of what the task involves (Example: `"Create a new repository, set up initial structure."`) 259 | - **status**: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`) 260 | - **dependencies**: IDs of prerequisite tasks (Example: `[1, 2.1]`) 261 | - Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending) 262 | - This helps quickly identify which prerequisite tasks are blocking work 263 | - **priority**: Importance level (Example: `"high"`, `"medium"`, `"low"`) 264 | - **details**: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`) 265 | - **testStrategy**: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`) 266 | - **subtasks**: List of smaller, more specific tasks (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`) 267 | - Refer to task structure details (previously linked to `tasks.md`). 268 | 269 | ## Configuration Management (Updated) 270 | 271 | Taskmaster configuration is managed through two main mechanisms: 272 | 273 | 1. **`.taskmaster/config.json` File (Primary):** 274 | * Located in the project root directory. 275 | * Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc. 276 | * **Tagged System Settings**: Includes `global.defaultTag` (defaults to "master") and `tags` section for tag management configuration. 277 | * **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing. 278 | * **View/Set specific models via `task-master models` command or `models` MCP tool.** 279 | * Created automatically when you run `task-master models --setup` for the first time or during tagged system migration. 280 | 281 | 2. **Environment Variables (`.env` / `mcp.json`):** 282 | * Used **only** for sensitive API keys and specific endpoint URLs. 283 | * Place API keys (one per provider) in a `.env` file in the project root for CLI usage. 284 | * For MCP/Kiro integration, configure these keys in the `env` section of `.kiro/mcp.json`. 285 | * Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.md`). 286 | 287 | 3. **`.taskmaster/state.json` File (Tagged System State):** 288 | * Tracks current tag context and migration status. 289 | * Automatically created during tagged system migration. 290 | * Contains: `currentTag`, `lastSwitched`, `migrationNoticeShown`. 291 | 292 | **Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool. 293 | **If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.kiro/mcp.json`. 294 | **If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project. 295 | 296 | ## Rules Management 297 | 298 | Taskmaster supports multiple AI coding assistant rule sets that can be configured during project initialization or managed afterward: 299 | 300 | - **Available Profiles**: Claude Code, Cline, Codex, Kiro, Roo Code, Trae, Windsurf (claude, cline, codex, kiro, roo, trae, windsurf) 301 | - **During Initialization**: Use `task-master init --rules kiro,windsurf` to specify which rule sets to include 302 | - **After Initialization**: Use `task-master rules add <profiles>` or `task-master rules remove <profiles>` to manage rule sets 303 | - **Interactive Setup**: Use `task-master rules setup` to launch an interactive prompt for selecting rule profiles 304 | - **Default Behavior**: If no `--rules` flag is specified during initialization, all available rule profiles are included 305 | - **Rule Structure**: Each profile creates its own directory (e.g., `.kiro/steering`, `.roo/rules`) with appropriate configuration files 306 | 307 | ## Determining the Next Task 308 | 309 | - Run `next_task` / `task-master next` to show the next task to work on. 310 | - The command identifies tasks with all dependencies satisfied 311 | - Tasks are prioritized by priority level, dependency count, and ID 312 | - The command shows comprehensive task information including: 313 | - Basic task details and description 314 | - Implementation details 315 | - Subtasks (if they exist) 316 | - Contextual suggested actions 317 | - Recommended before starting any new development work 318 | - Respects your project's dependency structure 319 | - Ensures tasks are completed in the appropriate sequence 320 | - Provides ready-to-use commands for common task actions 321 | 322 | ## Viewing Specific Task Details 323 | 324 | - Run `get_task` / `task-master show <id>` to view a specific task. 325 | - Use dot notation for subtasks: `task-master show 1.2` (shows subtask 2 of task 1) 326 | - Displays comprehensive information similar to the next command, but for a specific task 327 | - For parent tasks, shows all subtasks and their current status 328 | - For subtasks, shows parent task information and relationship 329 | - Provides contextual suggested actions appropriate for the specific task 330 | - Useful for examining task details before implementation or checking status 331 | 332 | ## Managing Task Dependencies 333 | 334 | - Use `add_dependency` / `task-master add-dependency --id=<id> --depends-on=<id>` to add a dependency. 335 | - Use `remove_dependency` / `task-master remove-dependency --id=<id> --depends-on=<id>` to remove a dependency. 336 | - The system prevents circular dependencies and duplicate dependency entries 337 | - Dependencies are checked for existence before being added or removed 338 | - Task files are automatically regenerated after dependency changes 339 | - Dependencies are visualized with status indicators in task listings and files 340 | 341 | ## Task Reorganization 342 | 343 | - Use `move_task` / `task-master move --from=<id> --to=<id>` to move tasks or subtasks within the hierarchy 344 | - This command supports several use cases: 345 | - Moving a standalone task to become a subtask (e.g., `--from=5 --to=7`) 346 | - Moving a subtask to become a standalone task (e.g., `--from=5.2 --to=7`) 347 | - Moving a subtask to a different parent (e.g., `--from=5.2 --to=7.3`) 348 | - Reordering subtasks within the same parent (e.g., `--from=5.2 --to=5.4`) 349 | - Moving a task to a new, non-existent ID position (e.g., `--from=5 --to=25`) 350 | - Moving multiple tasks at once using comma-separated IDs (e.g., `--from=10,11,12 --to=16,17,18`) 351 | - The system includes validation to prevent data loss: 352 | - Allows moving to non-existent IDs by creating placeholder tasks 353 | - Prevents moving to existing task IDs that have content (to avoid overwriting) 354 | - Validates source tasks exist before attempting to move them 355 | - The system maintains proper parent-child relationships and dependency integrity 356 | - Task files are automatically regenerated after the move operation 357 | - This provides greater flexibility in organizing and refining your task structure as project understanding evolves 358 | - This is especially useful when dealing with potential merge conflicts arising from teams creating tasks on separate branches. Solve these conflicts very easily by moving your tasks and keeping theirs. 359 | 360 | ## Iterative Subtask Implementation 361 | 362 | Once a task has been broken down into subtasks using `expand_task` or similar methods, follow this iterative process for implementation: 363 | 364 | 1. **Understand the Goal (Preparation):** 365 | * Use `get_task` / `task-master show <subtaskId>` (see @`taskmaster.md`) to thoroughly understand the specific goals and requirements of the subtask. 366 | 367 | 2. **Initial Exploration & Planning (Iteration 1):** 368 | * This is the first attempt at creating a concrete implementation plan. 369 | * Explore the codebase to identify the precise files, functions, and even specific lines of code that will need modification. 370 | * Determine the intended code changes (diffs) and their locations. 371 | * Gather *all* relevant details from this exploration phase. 372 | 373 | 3. **Log the Plan:** 374 | * Run `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<detailed plan>'`. 375 | * Provide the *complete and detailed* findings from the exploration phase in the prompt. Include file paths, line numbers, proposed diffs, reasoning, and any potential challenges identified. Do not omit details. The goal is to create a rich, timestamped log within the subtask's `details`. 376 | 377 | 4. **Verify the Plan:** 378 | * Run `get_task` / `task-master show <subtaskId>` again to confirm that the detailed implementation plan has been successfully appended to the subtask's details. 379 | 380 | 5. **Begin Implementation:** 381 | * Set the subtask status using `set_task_status` / `task-master set-status --id=<subtaskId> --status=in-progress`. 382 | * Start coding based on the logged plan. 383 | 384 | 6. **Refine and Log Progress (Iteration 2+):** 385 | * As implementation progresses, you will encounter challenges, discover nuances, or confirm successful approaches. 386 | * **Before appending new information**: Briefly review the *existing* details logged in the subtask (using `get_task` or recalling from context) to ensure the update adds fresh insights and avoids redundancy. 387 | * **Regularly** use `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<update details>\n- What worked...\n- What didn't work...'` to append new findings. 388 | * **Crucially, log:** 389 | * What worked ("fundamental truths" discovered). 390 | * What didn't work and why (to avoid repeating mistakes). 391 | * Specific code snippets or configurations that were successful. 392 | * Decisions made, especially if confirmed with user input. 393 | * Any deviations from the initial plan and the reasoning. 394 | * The objective is to continuously enrich the subtask's details, creating a log of the implementation journey that helps the AI (and human developers) learn, adapt, and avoid repeating errors. 395 | 396 | 7. **Review & Update Rules (Post-Implementation):** 397 | * Once the implementation for the subtask is functionally complete, review all code changes and the relevant chat history. 398 | * Identify any new or modified code patterns, conventions, or best practices established during the implementation. 399 | * Create new or update existing rules following internal guidelines (previously linked to `cursor_rules.md` and `self_improve.md`). 400 | 401 | 8. **Mark Task Complete:** 402 | * After verifying the implementation and updating any necessary rules, mark the subtask as completed: `set_task_status` / `task-master set-status --id=<subtaskId> --status=done`. 403 | 404 | 9. **Commit Changes (If using Git):** 405 | * Stage the relevant code changes and any updated/new rule files (`git add .`). 406 | * Craft a comprehensive Git commit message summarizing the work done for the subtask, including both code implementation and any rule adjustments. 407 | * Execute the commit command directly in the terminal (e.g., `git commit -m 'feat(module): Implement feature X for subtask <subtaskId>\n\n- Details about changes...\n- Updated rule Y for pattern Z'`). 408 | * Consider if a Changeset is needed according to internal versioning guidelines (previously linked to `changeset.md`). If so, run `npm run changeset`, stage the generated file, and amend the commit or create a new one. 409 | 410 | 10. **Proceed to Next Subtask:** 411 | * Identify the next subtask (e.g., using `next_task` / `task-master next`). 412 | 413 | ## Code Analysis & Refactoring Techniques 414 | 415 | - **Top-Level Function Search**: 416 | - Useful for understanding module structure or planning refactors. 417 | - Use grep/ripgrep to find exported functions/constants: 418 | `rg "export (async function|function|const) \w+"` or similar patterns. 419 | - Can help compare functions between files during migrations or identify potential naming conflicts. 420 | 421 | --- 422 | *This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.* ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/move-task.js: -------------------------------------------------------------------------------- ```javascript 1 | import path from 'path'; 2 | import { 3 | log, 4 | readJSON, 5 | writeJSON, 6 | setTasksForTag, 7 | traverseDependencies 8 | } from '../utils.js'; 9 | import generateTaskFiles from './generate-task-files.js'; 10 | import { 11 | findCrossTagDependencies, 12 | getDependentTaskIds, 13 | validateSubtaskMove 14 | } from '../dependency-manager.js'; 15 | 16 | /** 17 | * Find all dependencies recursively for a set of source tasks with depth limiting 18 | * @param {Array} sourceTasks - The source tasks to find dependencies for 19 | * @param {Array} allTasks - All available tasks from all tags 20 | * @param {Object} options - Options object 21 | * @param {number} options.maxDepth - Maximum recursion depth (default: 50) 22 | * @param {boolean} options.includeSelf - Whether to include self-references (default: false) 23 | * @returns {Array} Array of all dependency task IDs 24 | */ 25 | function findAllDependenciesRecursively(sourceTasks, allTasks, options = {}) { 26 | return traverseDependencies(sourceTasks, allTasks, { 27 | ...options, 28 | direction: 'forward', 29 | logger: { warn: console.warn } 30 | }); 31 | } 32 | 33 | /** 34 | * Structured error class for move operations 35 | */ 36 | class MoveTaskError extends Error { 37 | constructor(code, message, data = {}) { 38 | super(message); 39 | this.name = 'MoveTaskError'; 40 | this.code = code; 41 | this.data = data; 42 | } 43 | } 44 | 45 | /** 46 | * Error codes for move operations 47 | */ 48 | const MOVE_ERROR_CODES = { 49 | CROSS_TAG_DEPENDENCY_CONFLICTS: 'CROSS_TAG_DEPENDENCY_CONFLICTS', 50 | CANNOT_MOVE_SUBTASK: 'CANNOT_MOVE_SUBTASK', 51 | SOURCE_TARGET_TAGS_SAME: 'SOURCE_TARGET_TAGS_SAME', 52 | TASK_NOT_FOUND: 'TASK_NOT_FOUND', 53 | SUBTASK_NOT_FOUND: 'SUBTASK_NOT_FOUND', 54 | PARENT_TASK_NOT_FOUND: 'PARENT_TASK_NOT_FOUND', 55 | PARENT_TASK_NO_SUBTASKS: 'PARENT_TASK_NO_SUBTASKS', 56 | DESTINATION_TASK_NOT_FOUND: 'DESTINATION_TASK_NOT_FOUND', 57 | TASK_ALREADY_EXISTS: 'TASK_ALREADY_EXISTS', 58 | INVALID_TASKS_FILE: 'INVALID_TASKS_FILE', 59 | ID_COUNT_MISMATCH: 'ID_COUNT_MISMATCH', 60 | INVALID_SOURCE_TAG: 'INVALID_SOURCE_TAG', 61 | INVALID_TARGET_TAG: 'INVALID_TARGET_TAG' 62 | }; 63 | 64 | /** 65 | * Normalize a dependency value to its numeric parent task ID. 66 | * - Numbers are returned as-is (if finite) 67 | * - Numeric strings are parsed ("5" -> 5) 68 | * - Dotted strings return the parent portion ("5.2" -> 5) 69 | * - Empty/invalid values return null 70 | * - null/undefined are preserved 71 | * @param {number|string|null|undefined} dep 72 | * @returns {number|null|undefined} 73 | */ 74 | function normalizeDependency(dep) { 75 | if (dep === null || dep === undefined) return dep; 76 | if (typeof dep === 'number') return Number.isFinite(dep) ? dep : null; 77 | if (typeof dep === 'string') { 78 | const trimmed = dep.trim(); 79 | if (trimmed === '') return null; 80 | const parentPart = trimmed.includes('.') ? trimmed.split('.')[0] : trimmed; 81 | const parsed = parseInt(parentPart, 10); 82 | return Number.isFinite(parsed) ? parsed : null; 83 | } 84 | return null; 85 | } 86 | 87 | /** 88 | * Normalize an array of dependency values to numeric IDs. 89 | * Preserves null/undefined input (returns as-is) and filters out invalid entries. 90 | * @param {Array<any>|null|undefined} deps 91 | * @returns {Array<number>|null|undefined} 92 | */ 93 | function normalizeDependencies(deps) { 94 | if (deps === null || deps === undefined) return deps; 95 | if (!Array.isArray(deps)) return deps; 96 | return deps 97 | .map((d) => normalizeDependency(d)) 98 | .filter((n) => Number.isFinite(n)); 99 | } 100 | 101 | /** 102 | * Move one or more tasks/subtasks to new positions 103 | * @param {string} tasksPath - Path to tasks.json file 104 | * @param {string} sourceId - ID(s) of the task/subtask to move (e.g., '5' or '5.2' or '5,6,7') 105 | * @param {string} destinationId - ID(s) of the destination (e.g., '7' or '7.3' or '7,8,9') 106 | * @param {boolean} generateFiles - Whether to regenerate task files after moving 107 | * @param {Object} options - Additional options 108 | * @param {string} options.projectRoot - Project root directory for tag resolution 109 | * @param {string} options.tag - Explicit tag to use (optional) 110 | * @returns {Object} Result object with moved task details 111 | */ 112 | async function moveTask( 113 | tasksPath, 114 | sourceId, 115 | destinationId, 116 | generateFiles = false, 117 | options = {} 118 | ) { 119 | const { projectRoot, tag } = options; 120 | // Check if we have comma-separated IDs (batch move) 121 | const sourceIds = sourceId.split(',').map((id) => id.trim()); 122 | const destinationIds = destinationId.split(',').map((id) => id.trim()); 123 | 124 | if (sourceIds.length !== destinationIds.length) { 125 | throw new MoveTaskError( 126 | MOVE_ERROR_CODES.ID_COUNT_MISMATCH, 127 | `Number of source IDs (${sourceIds.length}) must match number of destination IDs (${destinationIds.length})` 128 | ); 129 | } 130 | 131 | // For batch moves, process each pair sequentially 132 | if (sourceIds.length > 1) { 133 | const results = []; 134 | for (let i = 0; i < sourceIds.length; i++) { 135 | const result = await moveTask( 136 | tasksPath, 137 | sourceIds[i], 138 | destinationIds[i], 139 | false, // Don't generate files for each individual move 140 | options 141 | ); 142 | results.push(result); 143 | } 144 | 145 | // Generate files once at the end if requested 146 | if (generateFiles) { 147 | await generateTaskFiles(tasksPath, path.dirname(tasksPath), { 148 | tag: tag, 149 | projectRoot: projectRoot 150 | }); 151 | } 152 | 153 | return { 154 | message: `Successfully moved ${sourceIds.length} tasks/subtasks`, 155 | moves: results 156 | }; 157 | } 158 | 159 | // Single move logic 160 | // Read the raw data without tag resolution to preserve tagged structure 161 | let rawData = readJSON(tasksPath, projectRoot, tag); 162 | 163 | // Handle the case where readJSON returns resolved data with _rawTaggedData 164 | if (rawData && rawData._rawTaggedData) { 165 | // Use the raw tagged data and discard the resolved view 166 | rawData = rawData._rawTaggedData; 167 | } 168 | 169 | // Ensure the tag exists in the raw data 170 | if (!rawData || !rawData[tag] || !Array.isArray(rawData[tag].tasks)) { 171 | throw new MoveTaskError( 172 | MOVE_ERROR_CODES.INVALID_TASKS_FILE, 173 | `Invalid tasks file or tag "${tag}" not found at ${tasksPath}` 174 | ); 175 | } 176 | 177 | // Get the tasks for the current tag 178 | const tasks = rawData[tag].tasks; 179 | 180 | log( 181 | 'info', 182 | `Moving task/subtask ${sourceId} to ${destinationId} (tag: ${tag})` 183 | ); 184 | 185 | // Parse source and destination IDs 186 | const isSourceSubtask = sourceId.includes('.'); 187 | const isDestSubtask = destinationId.includes('.'); 188 | 189 | let result; 190 | 191 | if (isSourceSubtask && isDestSubtask) { 192 | // Subtask to subtask 193 | result = moveSubtaskToSubtask(tasks, sourceId, destinationId); 194 | } else if (isSourceSubtask && !isDestSubtask) { 195 | // Subtask to task 196 | result = moveSubtaskToTask(tasks, sourceId, destinationId); 197 | } else if (!isSourceSubtask && isDestSubtask) { 198 | // Task to subtask 199 | result = moveTaskToSubtask(tasks, sourceId, destinationId); 200 | } else { 201 | // Task to task 202 | result = moveTaskToTask(tasks, sourceId, destinationId); 203 | } 204 | 205 | // Update the data structure with the modified tasks 206 | rawData[tag].tasks = tasks; 207 | 208 | // Always write the data object, never the _rawTaggedData directly 209 | // The writeJSON function will filter out _rawTaggedData automatically 210 | writeJSON(tasksPath, rawData, options.projectRoot, tag); 211 | 212 | if (generateFiles) { 213 | await generateTaskFiles(tasksPath, path.dirname(tasksPath), { 214 | tag: tag, 215 | projectRoot: projectRoot 216 | }); 217 | } 218 | 219 | return result; 220 | } 221 | 222 | // Helper functions for different move scenarios 223 | function moveSubtaskToSubtask(tasks, sourceId, destinationId) { 224 | // Parse IDs 225 | const [sourceParentId, sourceSubtaskId] = sourceId 226 | .split('.') 227 | .map((id) => parseInt(id, 10)); 228 | const [destParentId, destSubtaskId] = destinationId 229 | .split('.') 230 | .map((id) => parseInt(id, 10)); 231 | 232 | // Find source and destination parent tasks 233 | const sourceParentTask = tasks.find((t) => t.id === sourceParentId); 234 | const destParentTask = tasks.find((t) => t.id === destParentId); 235 | 236 | if (!sourceParentTask) { 237 | throw new MoveTaskError( 238 | MOVE_ERROR_CODES.PARENT_TASK_NOT_FOUND, 239 | `Source parent task with ID ${sourceParentId} not found` 240 | ); 241 | } 242 | if (!destParentTask) { 243 | throw new MoveTaskError( 244 | MOVE_ERROR_CODES.PARENT_TASK_NOT_FOUND, 245 | `Destination parent task with ID ${destParentId} not found` 246 | ); 247 | } 248 | 249 | // Initialize subtasks arrays if they don't exist (based on commit fixes) 250 | if (!sourceParentTask.subtasks) { 251 | sourceParentTask.subtasks = []; 252 | } 253 | if (!destParentTask.subtasks) { 254 | destParentTask.subtasks = []; 255 | } 256 | 257 | // Find source subtask 258 | const sourceSubtaskIndex = sourceParentTask.subtasks.findIndex( 259 | (st) => st.id === sourceSubtaskId 260 | ); 261 | if (sourceSubtaskIndex === -1) { 262 | throw new MoveTaskError( 263 | MOVE_ERROR_CODES.SUBTASK_NOT_FOUND, 264 | `Source subtask ${sourceId} not found` 265 | ); 266 | } 267 | 268 | const sourceSubtask = sourceParentTask.subtasks[sourceSubtaskIndex]; 269 | 270 | if (sourceParentId === destParentId) { 271 | // Moving within the same parent 272 | if (destParentTask.subtasks.length > 0) { 273 | const destSubtaskIndex = destParentTask.subtasks.findIndex( 274 | (st) => st.id === destSubtaskId 275 | ); 276 | if (destSubtaskIndex !== -1) { 277 | // Remove from old position 278 | sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1); 279 | // Insert at new position (adjust index if moving within same array) 280 | const adjustedIndex = 281 | sourceSubtaskIndex < destSubtaskIndex 282 | ? destSubtaskIndex - 1 283 | : destSubtaskIndex; 284 | destParentTask.subtasks.splice(adjustedIndex + 1, 0, sourceSubtask); 285 | } else { 286 | // Destination subtask doesn't exist, insert at end 287 | sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1); 288 | destParentTask.subtasks.push(sourceSubtask); 289 | } 290 | } else { 291 | // No existing subtasks, this will be the first one 292 | sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1); 293 | destParentTask.subtasks.push(sourceSubtask); 294 | } 295 | } else { 296 | // Moving between different parents 297 | moveSubtaskToAnotherParent( 298 | sourceSubtask, 299 | sourceParentTask, 300 | sourceSubtaskIndex, 301 | destParentTask, 302 | destSubtaskId 303 | ); 304 | } 305 | 306 | return { 307 | message: `Moved subtask ${sourceId} to ${destinationId}`, 308 | movedItem: sourceSubtask 309 | }; 310 | } 311 | 312 | function moveSubtaskToTask(tasks, sourceId, destinationId) { 313 | // Parse source ID 314 | const [sourceParentId, sourceSubtaskId] = sourceId 315 | .split('.') 316 | .map((id) => parseInt(id, 10)); 317 | const destTaskId = parseInt(destinationId, 10); 318 | 319 | // Find source parent and destination task 320 | const sourceParentTask = tasks.find((t) => t.id === sourceParentId); 321 | 322 | if (!sourceParentTask) { 323 | throw new MoveTaskError( 324 | MOVE_ERROR_CODES.PARENT_TASK_NOT_FOUND, 325 | `Source parent task with ID ${sourceParentId} not found` 326 | ); 327 | } 328 | if (!sourceParentTask.subtasks) { 329 | throw new MoveTaskError( 330 | MOVE_ERROR_CODES.PARENT_TASK_NO_SUBTASKS, 331 | `Source parent task ${sourceParentId} has no subtasks` 332 | ); 333 | } 334 | 335 | // Find source subtask 336 | const sourceSubtaskIndex = sourceParentTask.subtasks.findIndex( 337 | (st) => st.id === sourceSubtaskId 338 | ); 339 | if (sourceSubtaskIndex === -1) { 340 | throw new MoveTaskError( 341 | MOVE_ERROR_CODES.SUBTASK_NOT_FOUND, 342 | `Source subtask ${sourceId} not found` 343 | ); 344 | } 345 | 346 | const sourceSubtask = sourceParentTask.subtasks[sourceSubtaskIndex]; 347 | 348 | // Check if destination task exists 349 | const existingDestTask = tasks.find((t) => t.id === destTaskId); 350 | if (existingDestTask) { 351 | throw new MoveTaskError( 352 | MOVE_ERROR_CODES.TASK_ALREADY_EXISTS, 353 | `Cannot move to existing task ID ${destTaskId}. Choose a different ID or use subtask destination.` 354 | ); 355 | } 356 | 357 | // Create new task from subtask 358 | const newTask = { 359 | id: destTaskId, 360 | title: sourceSubtask.title, 361 | description: sourceSubtask.description, 362 | status: sourceSubtask.status || 'pending', 363 | dependencies: sourceSubtask.dependencies || [], 364 | priority: sourceSubtask.priority || 'medium', 365 | details: sourceSubtask.details || '', 366 | testStrategy: sourceSubtask.testStrategy || '', 367 | subtasks: [] 368 | }; 369 | 370 | // Remove subtask from source parent 371 | sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1); 372 | 373 | // Insert new task in correct position 374 | const insertIndex = tasks.findIndex((t) => t.id > destTaskId); 375 | if (insertIndex === -1) { 376 | tasks.push(newTask); 377 | } else { 378 | tasks.splice(insertIndex, 0, newTask); 379 | } 380 | 381 | return { 382 | message: `Converted subtask ${sourceId} to task ${destinationId}`, 383 | movedItem: newTask 384 | }; 385 | } 386 | 387 | function moveTaskToSubtask(tasks, sourceId, destinationId) { 388 | // Parse IDs 389 | const sourceTaskId = parseInt(sourceId, 10); 390 | const [destParentId, destSubtaskId] = destinationId 391 | .split('.') 392 | .map((id) => parseInt(id, 10)); 393 | 394 | // Find source task and destination parent 395 | const sourceTaskIndex = tasks.findIndex((t) => t.id === sourceTaskId); 396 | const destParentTask = tasks.find((t) => t.id === destParentId); 397 | 398 | if (sourceTaskIndex === -1) { 399 | throw new MoveTaskError( 400 | MOVE_ERROR_CODES.TASK_NOT_FOUND, 401 | `Source task with ID ${sourceTaskId} not found` 402 | ); 403 | } 404 | if (!destParentTask) { 405 | throw new MoveTaskError( 406 | MOVE_ERROR_CODES.PARENT_TASK_NOT_FOUND, 407 | `Destination parent task with ID ${destParentId} not found` 408 | ); 409 | } 410 | 411 | const sourceTask = tasks[sourceTaskIndex]; 412 | 413 | // Initialize subtasks array if it doesn't exist (based on commit fixes) 414 | if (!destParentTask.subtasks) { 415 | destParentTask.subtasks = []; 416 | } 417 | 418 | // Create new subtask from task 419 | const newSubtask = { 420 | id: destSubtaskId, 421 | title: sourceTask.title, 422 | description: sourceTask.description, 423 | status: sourceTask.status || 'pending', 424 | dependencies: sourceTask.dependencies || [], 425 | details: sourceTask.details || '', 426 | testStrategy: sourceTask.testStrategy || '' 427 | }; 428 | 429 | // Find insertion position (based on commit fixes) 430 | let destSubtaskIndex = -1; 431 | if (destParentTask.subtasks.length > 0) { 432 | destSubtaskIndex = destParentTask.subtasks.findIndex( 433 | (st) => st.id === destSubtaskId 434 | ); 435 | if (destSubtaskIndex === -1) { 436 | // Subtask doesn't exist, we'll insert at the end 437 | destSubtaskIndex = destParentTask.subtasks.length - 1; 438 | } 439 | } 440 | 441 | // Insert at specific position (based on commit fixes) 442 | const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1; 443 | destParentTask.subtasks.splice(insertPosition, 0, newSubtask); 444 | 445 | // Remove the original task from the tasks array 446 | tasks.splice(sourceTaskIndex, 1); 447 | 448 | return { 449 | message: `Converted task ${sourceId} to subtask ${destinationId}`, 450 | movedItem: newSubtask 451 | }; 452 | } 453 | 454 | function moveTaskToTask(tasks, sourceId, destinationId) { 455 | const sourceTaskId = parseInt(sourceId, 10); 456 | const destTaskId = parseInt(destinationId, 10); 457 | 458 | // Find source task 459 | const sourceTaskIndex = tasks.findIndex((t) => t.id === sourceTaskId); 460 | if (sourceTaskIndex === -1) { 461 | throw new MoveTaskError( 462 | MOVE_ERROR_CODES.TASK_NOT_FOUND, 463 | `Source task with ID ${sourceTaskId} not found` 464 | ); 465 | } 466 | 467 | const sourceTask = tasks[sourceTaskIndex]; 468 | 469 | // Check if destination exists 470 | const destTaskIndex = tasks.findIndex((t) => t.id === destTaskId); 471 | 472 | if (destTaskIndex !== -1) { 473 | // Destination exists - this could be overwriting or swapping 474 | const destTask = tasks[destTaskIndex]; 475 | 476 | // For now, throw an error to avoid accidental overwrites 477 | throw new MoveTaskError( 478 | MOVE_ERROR_CODES.TASK_ALREADY_EXISTS, 479 | `Task with ID ${destTaskId} already exists. Use a different destination ID.` 480 | ); 481 | } else { 482 | // Destination doesn't exist - create new task ID 483 | return moveTaskToNewId(tasks, sourceTaskIndex, sourceTask, destTaskId); 484 | } 485 | } 486 | 487 | function moveSubtaskToAnotherParent( 488 | sourceSubtask, 489 | sourceParentTask, 490 | sourceSubtaskIndex, 491 | destParentTask, 492 | destSubtaskId 493 | ) { 494 | const destSubtaskId_num = parseInt(destSubtaskId, 10); 495 | 496 | // Create new subtask with destination ID 497 | const newSubtask = { 498 | ...sourceSubtask, 499 | id: destSubtaskId_num 500 | }; 501 | 502 | // Initialize subtasks array if it doesn't exist (based on commit fixes) 503 | if (!destParentTask.subtasks) { 504 | destParentTask.subtasks = []; 505 | } 506 | 507 | // Find insertion position 508 | let destSubtaskIndex = -1; 509 | if (destParentTask.subtasks.length > 0) { 510 | destSubtaskIndex = destParentTask.subtasks.findIndex( 511 | (st) => st.id === destSubtaskId_num 512 | ); 513 | if (destSubtaskIndex === -1) { 514 | // Subtask doesn't exist, we'll insert at the end 515 | destSubtaskIndex = destParentTask.subtasks.length - 1; 516 | } 517 | } 518 | 519 | // Insert at the destination position (based on commit fixes) 520 | const insertPosition = destSubtaskIndex === -1 ? 0 : destSubtaskIndex + 1; 521 | destParentTask.subtasks.splice(insertPosition, 0, newSubtask); 522 | 523 | // Remove the subtask from the original parent 524 | sourceParentTask.subtasks.splice(sourceSubtaskIndex, 1); 525 | 526 | return newSubtask; 527 | } 528 | 529 | function moveTaskToNewId(tasks, sourceTaskIndex, sourceTask, destTaskId) { 530 | const destTaskIndex = tasks.findIndex((t) => t.id === destTaskId); 531 | 532 | // Create moved task with new ID 533 | const movedTask = { 534 | ...sourceTask, 535 | id: destTaskId 536 | }; 537 | 538 | // Update any dependencies that reference the old task ID 539 | tasks.forEach((task) => { 540 | if (task.dependencies && task.dependencies.includes(sourceTask.id)) { 541 | const depIndex = task.dependencies.indexOf(sourceTask.id); 542 | task.dependencies[depIndex] = destTaskId; 543 | } 544 | if (task.subtasks) { 545 | task.subtasks.forEach((subtask) => { 546 | if ( 547 | subtask.dependencies && 548 | subtask.dependencies.includes(sourceTask.id) 549 | ) { 550 | const depIndex = subtask.dependencies.indexOf(sourceTask.id); 551 | subtask.dependencies[depIndex] = destTaskId; 552 | } 553 | }); 554 | } 555 | }); 556 | 557 | // Update dependencies within movedTask's subtasks that reference sibling subtasks 558 | if (Array.isArray(movedTask.subtasks)) { 559 | movedTask.subtasks.forEach((subtask) => { 560 | if (Array.isArray(subtask.dependencies)) { 561 | subtask.dependencies = subtask.dependencies.map((dep) => { 562 | // If dependency is a string like "oldParent.subId", update to "newParent.subId" 563 | if (typeof dep === 'string' && dep.includes('.')) { 564 | const [depParent, depSub] = dep.split('.'); 565 | if (parseInt(depParent, 10) === sourceTask.id) { 566 | return `${destTaskId}.${depSub}`; 567 | } 568 | } 569 | // If dependency is a number, and matches a subtask ID in the moved task, leave as is (context is implied) 570 | return dep; 571 | }); 572 | } 573 | }); 574 | } 575 | 576 | // Strategy based on commit fixes: remove source first, then replace destination 577 | // This avoids index shifting problems 578 | 579 | // Remove the source task first 580 | tasks.splice(sourceTaskIndex, 1); 581 | 582 | // Adjust the destination index if the source was before the destination 583 | // Since we removed the source, indices after it shift down by 1 584 | const adjustedDestIndex = 585 | sourceTaskIndex < destTaskIndex ? destTaskIndex - 1 : destTaskIndex; 586 | 587 | // Replace the placeholder destination task with the moved task (based on commit fixes) 588 | if (adjustedDestIndex >= 0 && adjustedDestIndex < tasks.length) { 589 | tasks[adjustedDestIndex] = movedTask; 590 | } else { 591 | // Insert at the end if index is out of bounds 592 | tasks.push(movedTask); 593 | } 594 | 595 | log('info', `Moved task ${sourceTask.id} to new ID ${destTaskId}`); 596 | 597 | return { 598 | message: `Moved task ${sourceTask.id} to new ID ${destTaskId}`, 599 | movedItem: movedTask 600 | }; 601 | } 602 | 603 | /** 604 | * Get all tasks from all tags with tag information 605 | * @param {Object} rawData - The raw tagged data object 606 | * @returns {Array} A flat array of all task objects with tag property 607 | */ 608 | function getAllTasksWithTags(rawData) { 609 | let allTasks = []; 610 | for (const tagName in rawData) { 611 | if ( 612 | Object.prototype.hasOwnProperty.call(rawData, tagName) && 613 | rawData[tagName] && 614 | Array.isArray(rawData[tagName].tasks) 615 | ) { 616 | const tasksWithTag = rawData[tagName].tasks.map((task) => ({ 617 | ...task, 618 | tag: tagName 619 | })); 620 | allTasks = allTasks.concat(tasksWithTag); 621 | } 622 | } 623 | return allTasks; 624 | } 625 | 626 | /** 627 | * Validate move operation parameters and data 628 | * @param {string} tasksPath - Path to tasks.json file 629 | * @param {Array} taskIds - Array of task IDs to move 630 | * @param {string} sourceTag - Source tag name 631 | * @param {string} targetTag - Target tag name 632 | * @param {Object} context - Context object 633 | * @returns {Object} Validation result with rawData and sourceTasks 634 | */ 635 | async function validateMove(tasksPath, taskIds, sourceTag, targetTag, context) { 636 | const { projectRoot } = context; 637 | 638 | // Read the raw data without tag resolution to preserve tagged structure 639 | let rawData = readJSON(tasksPath, projectRoot, sourceTag); 640 | 641 | // Handle the case where readJSON returns resolved data with _rawTaggedData 642 | if (rawData && rawData._rawTaggedData) { 643 | rawData = rawData._rawTaggedData; 644 | } 645 | 646 | // Validate source tag exists 647 | if ( 648 | !rawData || 649 | !rawData[sourceTag] || 650 | !Array.isArray(rawData[sourceTag].tasks) 651 | ) { 652 | throw new MoveTaskError( 653 | MOVE_ERROR_CODES.INVALID_SOURCE_TAG, 654 | `Source tag "${sourceTag}" not found or invalid` 655 | ); 656 | } 657 | 658 | // Create target tag if it doesn't exist 659 | if (!rawData[targetTag]) { 660 | rawData[targetTag] = { tasks: [] }; 661 | log('info', `Created new tag "${targetTag}"`); 662 | } 663 | 664 | // Normalize all IDs to strings once for consistent comparison 665 | const normalizedSearchIds = taskIds.map((id) => String(id)); 666 | 667 | const sourceTasks = rawData[sourceTag].tasks.filter((t) => { 668 | const normalizedTaskId = String(t.id); 669 | return normalizedSearchIds.includes(normalizedTaskId); 670 | }); 671 | 672 | // Validate subtask movement 673 | taskIds.forEach((taskId) => { 674 | validateSubtaskMove(taskId, sourceTag, targetTag); 675 | }); 676 | 677 | return { rawData, sourceTasks }; 678 | } 679 | 680 | /** 681 | * Load and prepare task data for move operation 682 | * @param {Object} validation - Validation result from validateMove 683 | * @returns {Object} Prepared data with rawData, sourceTasks, and allTasks 684 | */ 685 | async function prepareTaskData(validation) { 686 | const { rawData, sourceTasks } = validation; 687 | 688 | // Get all tasks for validation 689 | const allTasks = getAllTasksWithTags(rawData); 690 | 691 | return { rawData, sourceTasks, allTasks }; 692 | } 693 | 694 | /** 695 | * Resolve dependencies and determine tasks to move 696 | * @param {Array} sourceTasks - Source tasks to move 697 | * @param {Array} allTasks - All available tasks from all tags 698 | * @param {Object} options - Move options 699 | * @param {Array} taskIds - Original task IDs 700 | * @param {string} sourceTag - Source tag name 701 | * @param {string} targetTag - Target tag name 702 | * @returns {Object} Tasks to move and dependency resolution info 703 | */ 704 | async function resolveDependencies( 705 | sourceTasks, 706 | allTasks, 707 | options, 708 | taskIds, 709 | sourceTag, 710 | targetTag 711 | ) { 712 | const { withDependencies = false, ignoreDependencies = false } = options; 713 | 714 | // Scope allTasks to the source tag to avoid cross-tag contamination when 715 | // computing dependency chains for --with-dependencies 716 | const tasksInSourceTag = Array.isArray(allTasks) 717 | ? allTasks.filter((t) => t && t.tag === sourceTag) 718 | : []; 719 | 720 | // Handle --with-dependencies flag first (regardless of cross-tag dependencies) 721 | if (withDependencies) { 722 | // Move dependent tasks along with main tasks 723 | // Find ALL dependencies recursively, but only using tasks from the source tag 724 | const allDependentTaskIdsRaw = findAllDependenciesRecursively( 725 | sourceTasks, 726 | tasksInSourceTag, 727 | { maxDepth: 100, includeSelf: false } 728 | ); 729 | 730 | // Filter dependent IDs to those that actually exist in the source tag 731 | const sourceTagIds = new Set( 732 | tasksInSourceTag.map((t) => 733 | typeof t.id === 'string' ? parseInt(t.id, 10) : t.id 734 | ) 735 | ); 736 | const allDependentTaskIds = allDependentTaskIdsRaw.filter((depId) => { 737 | // Only numeric task IDs are eligible to be moved (subtasks cannot be moved cross-tag) 738 | const normalizedId = normalizeDependency(depId); 739 | return Number.isFinite(normalizedId) && sourceTagIds.has(normalizedId); 740 | }); 741 | 742 | const allTaskIdsToMove = [...new Set([...taskIds, ...allDependentTaskIds])]; 743 | 744 | log( 745 | 'info', 746 | `Moving ${allTaskIdsToMove.length} tasks (including dependencies): ${allTaskIdsToMove.join(', ')}` 747 | ); 748 | 749 | return { 750 | tasksToMove: allTaskIdsToMove, 751 | dependencyResolution: { 752 | type: 'with-dependencies', 753 | dependentTasks: allDependentTaskIds 754 | } 755 | }; 756 | } 757 | 758 | // Find cross-tag dependencies (these shouldn't exist since dependencies are only within tags) 759 | const crossTagDependencies = findCrossTagDependencies( 760 | sourceTasks, 761 | sourceTag, 762 | targetTag, 763 | allTasks 764 | ); 765 | 766 | if (crossTagDependencies.length > 0) { 767 | if (ignoreDependencies) { 768 | // Break cross-tag dependencies (edge case - shouldn't normally happen) 769 | sourceTasks.forEach((task) => { 770 | const sourceTagTasks = tasksInSourceTag; 771 | const targetTagTasks = Array.isArray(allTasks) 772 | ? allTasks.filter((t) => t && t.tag === targetTag) 773 | : []; 774 | task.dependencies = task.dependencies.filter((depId) => { 775 | const parentTaskId = normalizeDependency(depId); 776 | 777 | // If dependency resolves to a task in the source tag, drop it (would be cross-tag after move) 778 | if ( 779 | Number.isFinite(parentTaskId) && 780 | sourceTagTasks.some((t) => t.id === parentTaskId) 781 | ) { 782 | return false; 783 | } 784 | 785 | // If dependency resolves to a task in the target tag, keep it 786 | if ( 787 | Number.isFinite(parentTaskId) && 788 | targetTagTasks.some((t) => t.id === parentTaskId) 789 | ) { 790 | return true; 791 | } 792 | 793 | // Otherwise, keep as-is (unknown/unresolved dependency) 794 | return true; 795 | }); 796 | }); 797 | 798 | log( 799 | 'warn', 800 | `Removed ${crossTagDependencies.length} cross-tag dependencies` 801 | ); 802 | 803 | return { 804 | tasksToMove: taskIds, 805 | dependencyResolution: { 806 | type: 'ignored-dependencies', 807 | conflicts: crossTagDependencies 808 | } 809 | }; 810 | } else { 811 | // Block move and show error 812 | throw new MoveTaskError( 813 | MOVE_ERROR_CODES.CROSS_TAG_DEPENDENCY_CONFLICTS, 814 | `Cannot move tasks: ${crossTagDependencies.length} cross-tag dependency conflicts found`, 815 | { 816 | conflicts: crossTagDependencies, 817 | sourceTag, 818 | targetTag, 819 | taskIds 820 | } 821 | ); 822 | } 823 | } 824 | 825 | return { 826 | tasksToMove: taskIds, 827 | dependencyResolution: { type: 'no-conflicts' } 828 | }; 829 | } 830 | 831 | /** 832 | * Execute the actual move operation 833 | * @param {Array} tasksToMove - Array of task IDs to move 834 | * @param {string} sourceTag - Source tag name 835 | * @param {string} targetTag - Target tag name 836 | * @param {Object} rawData - Raw data object 837 | * @param {Object} context - Context object 838 | * @param {string} tasksPath - Path to tasks.json file 839 | * @returns {Object} Move operation result 840 | */ 841 | async function executeMoveOperation( 842 | tasksToMove, 843 | sourceTag, 844 | targetTag, 845 | rawData, 846 | context, 847 | tasksPath 848 | ) { 849 | const { projectRoot } = context; 850 | const movedTasks = []; 851 | 852 | // Move each task from source to target tag 853 | for (const taskId of tasksToMove) { 854 | // Normalize taskId to number for comparison 855 | const normalizedTaskId = 856 | typeof taskId === 'string' ? parseInt(taskId, 10) : taskId; 857 | 858 | const sourceTaskIndex = rawData[sourceTag].tasks.findIndex( 859 | (t) => t.id === normalizedTaskId 860 | ); 861 | 862 | if (sourceTaskIndex === -1) { 863 | throw new MoveTaskError( 864 | MOVE_ERROR_CODES.TASK_NOT_FOUND, 865 | `Task ${taskId} not found in source tag "${sourceTag}"` 866 | ); 867 | } 868 | 869 | const taskToMove = rawData[sourceTag].tasks[sourceTaskIndex]; 870 | 871 | // Check for ID conflicts in target tag 872 | const existingTaskIndex = rawData[targetTag].tasks.findIndex( 873 | (t) => t.id === normalizedTaskId 874 | ); 875 | if (existingTaskIndex !== -1) { 876 | throw new MoveTaskError( 877 | MOVE_ERROR_CODES.TASK_ALREADY_EXISTS, 878 | `Task ${taskId} already exists in target tag "${targetTag}"`, 879 | { 880 | conflictingId: normalizedTaskId, 881 | targetTag, 882 | suggestions: [ 883 | 'Choose a different target tag without conflicting IDs', 884 | 'Move a different set of IDs (avoid existing ones)', 885 | 'If needed, move within-tag to a new ID first, then cross-tag move' 886 | ] 887 | } 888 | ); 889 | } 890 | 891 | // Remove from source tag 892 | rawData[sourceTag].tasks.splice(sourceTaskIndex, 1); 893 | 894 | // Preserve task metadata and add to target tag 895 | const taskWithPreservedMetadata = preserveTaskMetadata( 896 | taskToMove, 897 | sourceTag, 898 | targetTag 899 | ); 900 | rawData[targetTag].tasks.push(taskWithPreservedMetadata); 901 | 902 | movedTasks.push({ 903 | id: taskId, 904 | fromTag: sourceTag, 905 | toTag: targetTag 906 | }); 907 | 908 | log('info', `Moved task ${taskId} from "${sourceTag}" to "${targetTag}"`); 909 | } 910 | 911 | return { rawData, movedTasks }; 912 | } 913 | 914 | /** 915 | * Finalize the move operation by saving data and returning result 916 | * @param {Object} moveResult - Result from executeMoveOperation 917 | * @param {string} tasksPath - Path to tasks.json file 918 | * @param {Object} context - Context object 919 | * @param {string} sourceTag - Source tag name 920 | * @param {string} targetTag - Target tag name 921 | * @returns {Object} Final result object 922 | */ 923 | async function finalizeMove( 924 | moveResult, 925 | tasksPath, 926 | context, 927 | sourceTag, 928 | targetTag, 929 | dependencyResolution 930 | ) { 931 | const { projectRoot } = context; 932 | const { rawData, movedTasks } = moveResult; 933 | 934 | // Write the updated data 935 | writeJSON(tasksPath, rawData, projectRoot, null); 936 | 937 | const response = { 938 | message: `Successfully moved ${movedTasks.length} tasks from "${sourceTag}" to "${targetTag}"`, 939 | movedTasks 940 | }; 941 | 942 | // If we intentionally broke cross-tag dependencies, provide tips to validate & fix 943 | if ( 944 | dependencyResolution && 945 | dependencyResolution.type === 'ignored-dependencies' 946 | ) { 947 | response.tips = [ 948 | 'Run "task-master validate-dependencies" to check for dependency issues.', 949 | 'Run "task-master fix-dependencies" to automatically repair dangling dependencies.' 950 | ]; 951 | } 952 | 953 | return response; 954 | } 955 | 956 | /** 957 | * Move tasks between different tags with dependency handling 958 | * @param {string} tasksPath - Path to tasks.json file 959 | * @param {Array} taskIds - Array of task IDs to move 960 | * @param {string} sourceTag - Source tag name 961 | * @param {string} targetTag - Target tag name 962 | * @param {Object} options - Move options 963 | * @param {boolean} options.withDependencies - Move dependent tasks along with main task 964 | * @param {boolean} options.ignoreDependencies - Break cross-tag dependencies during move 965 | * @param {Object} context - Context object containing projectRoot and tag information 966 | * @returns {Object} Result object with moved task details 967 | */ 968 | async function moveTasksBetweenTags( 969 | tasksPath, 970 | taskIds, 971 | sourceTag, 972 | targetTag, 973 | options = {}, 974 | context = {} 975 | ) { 976 | // 1. Validation phase 977 | const validation = await validateMove( 978 | tasksPath, 979 | taskIds, 980 | sourceTag, 981 | targetTag, 982 | context 983 | ); 984 | 985 | // 2. Load and prepare data 986 | const { rawData, sourceTasks, allTasks } = await prepareTaskData(validation); 987 | 988 | // 3. Handle dependencies 989 | const { tasksToMove, dependencyResolution } = await resolveDependencies( 990 | sourceTasks, 991 | allTasks, 992 | options, 993 | taskIds, 994 | sourceTag, 995 | targetTag 996 | ); 997 | 998 | // 4. Execute move 999 | const moveResult = await executeMoveOperation( 1000 | tasksToMove, 1001 | sourceTag, 1002 | targetTag, 1003 | rawData, 1004 | context, 1005 | tasksPath 1006 | ); 1007 | 1008 | // 5. Save and return 1009 | return await finalizeMove( 1010 | moveResult, 1011 | tasksPath, 1012 | context, 1013 | sourceTag, 1014 | targetTag, 1015 | dependencyResolution 1016 | ); 1017 | } 1018 | 1019 | /** 1020 | * Detect ID conflicts in target tag 1021 | * @param {Array} taskIds - Array of task IDs to check 1022 | * @param {string} targetTag - Target tag name 1023 | * @param {Object} rawData - Raw data object 1024 | * @returns {Array} Array of conflicting task IDs 1025 | */ 1026 | function detectIdConflicts(taskIds, targetTag, rawData) { 1027 | const conflicts = []; 1028 | 1029 | if (!rawData[targetTag] || !Array.isArray(rawData[targetTag].tasks)) { 1030 | return conflicts; 1031 | } 1032 | 1033 | taskIds.forEach((taskId) => { 1034 | // Normalize taskId to number for comparison 1035 | const normalizedTaskId = 1036 | typeof taskId === 'string' ? parseInt(taskId, 10) : taskId; 1037 | const existingTask = rawData[targetTag].tasks.find( 1038 | (t) => t.id === normalizedTaskId 1039 | ); 1040 | if (existingTask) { 1041 | conflicts.push(taskId); 1042 | } 1043 | }); 1044 | 1045 | return conflicts; 1046 | } 1047 | 1048 | /** 1049 | * Preserve task metadata during cross-tag moves 1050 | * @param {Object} task - Task object 1051 | * @param {string} sourceTag - Source tag name 1052 | * @param {string} targetTag - Target tag name 1053 | * @returns {Object} Task object with preserved metadata 1054 | */ 1055 | function preserveTaskMetadata(task, sourceTag, targetTag) { 1056 | // Update the tag property to reflect the new location 1057 | task.tag = targetTag; 1058 | 1059 | // Add move history to task metadata 1060 | if (!task.metadata) { 1061 | task.metadata = {}; 1062 | } 1063 | 1064 | if (!task.metadata.moveHistory) { 1065 | task.metadata.moveHistory = []; 1066 | } 1067 | 1068 | task.metadata.moveHistory.push({ 1069 | fromTag: sourceTag, 1070 | toTag: targetTag, 1071 | timestamp: new Date().toISOString() 1072 | }); 1073 | 1074 | return task; 1075 | } 1076 | 1077 | export default moveTask; 1078 | export { 1079 | moveTasksBetweenTags, 1080 | getAllTasksWithTags, 1081 | detectIdConflicts, 1082 | preserveTaskMetadata, 1083 | MoveTaskError, 1084 | MOVE_ERROR_CODES 1085 | }; 1086 | ```