This is page 32 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 -------------------------------------------------------------------------------- /apps/cli/src/commands/context.command.ts: -------------------------------------------------------------------------------- ```typescript 1 | /** 2 | * @fileoverview Context command for managing org/brief selection 3 | * Provides a clean interface for workspace context management 4 | */ 5 | 6 | import { Command } from 'commander'; 7 | import chalk from 'chalk'; 8 | import inquirer from 'inquirer'; 9 | import ora, { Ora } from 'ora'; 10 | import { 11 | AuthManager, 12 | AuthenticationError, 13 | type UserContext 14 | } from '@tm/core/auth'; 15 | import * as ui from '../utils/ui.js'; 16 | 17 | /** 18 | * Result type from context command 19 | */ 20 | export interface ContextResult { 21 | success: boolean; 22 | action: 'show' | 'select-org' | 'select-brief' | 'clear' | 'set'; 23 | context?: UserContext; 24 | message?: string; 25 | } 26 | 27 | /** 28 | * ContextCommand extending Commander's Command class 29 | * Manages user's workspace context (org/brief selection) 30 | */ 31 | export class ContextCommand extends Command { 32 | private authManager: AuthManager; 33 | private lastResult?: ContextResult; 34 | 35 | constructor(name?: string) { 36 | super(name || 'context'); 37 | 38 | // Initialize auth manager 39 | this.authManager = AuthManager.getInstance(); 40 | 41 | // Configure the command 42 | this.description( 43 | 'Manage workspace context (organization and brief selection)' 44 | ); 45 | 46 | // Add subcommands 47 | this.addOrgCommand(); 48 | this.addBriefCommand(); 49 | this.addClearCommand(); 50 | this.addSetCommand(); 51 | 52 | // Accept optional positional argument for brief ID or Hamster URL 53 | this.argument('[briefOrUrl]', 'Brief ID or Hamster brief URL'); 54 | 55 | // Default action: if an argument is provided, resolve and set context; else show 56 | this.action(async (briefOrUrl?: string) => { 57 | if (briefOrUrl && briefOrUrl.trim().length > 0) { 58 | await this.executeSetFromBriefInput(briefOrUrl.trim()); 59 | return; 60 | } 61 | await this.executeShow(); 62 | }); 63 | } 64 | 65 | /** 66 | * Add org selection subcommand 67 | */ 68 | private addOrgCommand(): void { 69 | this.command('org') 70 | .description('Select an organization') 71 | .action(async () => { 72 | await this.executeSelectOrg(); 73 | }); 74 | } 75 | 76 | /** 77 | * Add brief selection subcommand 78 | */ 79 | private addBriefCommand(): void { 80 | this.command('brief') 81 | .description('Select a brief within the current organization') 82 | .action(async () => { 83 | await this.executeSelectBrief(); 84 | }); 85 | } 86 | 87 | /** 88 | * Add clear subcommand 89 | */ 90 | private addClearCommand(): void { 91 | this.command('clear') 92 | .description('Clear all context selections') 93 | .action(async () => { 94 | await this.executeClear(); 95 | }); 96 | } 97 | 98 | /** 99 | * Add set subcommand for direct context setting 100 | */ 101 | private addSetCommand(): void { 102 | this.command('set') 103 | .description('Set context directly') 104 | .option('--org <id>', 'Organization ID') 105 | .option('--org-name <name>', 'Organization name') 106 | .option('--brief <id>', 'Brief ID') 107 | .option('--brief-name <name>', 'Brief name') 108 | .action(async (options) => { 109 | await this.executeSet(options); 110 | }); 111 | } 112 | 113 | /** 114 | * Execute show current context 115 | */ 116 | private async executeShow(): Promise<void> { 117 | try { 118 | const result = this.displayContext(); 119 | this.setLastResult(result); 120 | } catch (error: any) { 121 | this.handleError(error); 122 | process.exit(1); 123 | } 124 | } 125 | 126 | /** 127 | * Display current context 128 | */ 129 | private displayContext(): ContextResult { 130 | // Check authentication first 131 | if (!this.authManager.isAuthenticated()) { 132 | console.log(chalk.yellow('✗ Not authenticated')); 133 | console.log(chalk.gray('\n Run "tm auth login" to authenticate first')); 134 | 135 | return { 136 | success: false, 137 | action: 'show', 138 | message: 'Not authenticated' 139 | }; 140 | } 141 | 142 | const context = this.authManager.getContext(); 143 | 144 | console.log(chalk.cyan('\n🌍 Workspace Context\n')); 145 | 146 | if (context && (context.orgId || context.briefId)) { 147 | if (context.orgName || context.orgId) { 148 | console.log(chalk.green('✓ Organization')); 149 | if (context.orgName) { 150 | console.log(chalk.white(` ${context.orgName}`)); 151 | } 152 | if (context.orgId) { 153 | console.log(chalk.gray(` ID: ${context.orgId}`)); 154 | } 155 | } 156 | 157 | if (context.briefName || context.briefId) { 158 | console.log(chalk.green('\n✓ Brief')); 159 | if (context.briefName) { 160 | console.log(chalk.white(` ${context.briefName}`)); 161 | } 162 | if (context.briefId) { 163 | console.log(chalk.gray(` ID: ${context.briefId}`)); 164 | } 165 | } 166 | 167 | if (context.updatedAt) { 168 | console.log( 169 | chalk.gray( 170 | `\n Last updated: ${new Date(context.updatedAt).toLocaleString()}` 171 | ) 172 | ); 173 | } 174 | 175 | return { 176 | success: true, 177 | action: 'show', 178 | context, 179 | message: 'Context loaded' 180 | }; 181 | } else { 182 | console.log(chalk.yellow('✗ No context selected')); 183 | console.log( 184 | chalk.gray('\n Run "tm context org" to select an organization') 185 | ); 186 | console.log(chalk.gray(' Run "tm context brief" to select a brief')); 187 | 188 | return { 189 | success: true, 190 | action: 'show', 191 | message: 'No context selected' 192 | }; 193 | } 194 | } 195 | 196 | /** 197 | * Execute org selection 198 | */ 199 | private async executeSelectOrg(): Promise<void> { 200 | try { 201 | // Check authentication 202 | if (!this.authManager.isAuthenticated()) { 203 | ui.displayError('Not authenticated. Run "tm auth login" first.'); 204 | process.exit(1); 205 | } 206 | 207 | const result = await this.selectOrganization(); 208 | this.setLastResult(result); 209 | 210 | if (!result.success) { 211 | process.exit(1); 212 | } 213 | } catch (error: any) { 214 | this.handleError(error); 215 | process.exit(1); 216 | } 217 | } 218 | 219 | /** 220 | * Select an organization interactively 221 | */ 222 | private async selectOrganization(): Promise<ContextResult> { 223 | const spinner = ora('Fetching organizations...').start(); 224 | 225 | try { 226 | // Fetch organizations from API 227 | const organizations = await this.authManager.getOrganizations(); 228 | spinner.stop(); 229 | 230 | if (organizations.length === 0) { 231 | ui.displayWarning('No organizations available'); 232 | return { 233 | success: false, 234 | action: 'select-org', 235 | message: 'No organizations available' 236 | }; 237 | } 238 | 239 | // Prompt for selection 240 | const { selectedOrg } = await inquirer.prompt([ 241 | { 242 | type: 'list', 243 | name: 'selectedOrg', 244 | message: 'Select an organization:', 245 | choices: organizations.map((org) => ({ 246 | name: org.name, 247 | value: org 248 | })) 249 | } 250 | ]); 251 | 252 | // Update context 253 | await this.authManager.updateContext({ 254 | orgId: selectedOrg.id, 255 | orgName: selectedOrg.name, 256 | // Clear brief when changing org 257 | briefId: undefined, 258 | briefName: undefined 259 | }); 260 | 261 | ui.displaySuccess(`Selected organization: ${selectedOrg.name}`); 262 | 263 | return { 264 | success: true, 265 | action: 'select-org', 266 | context: this.authManager.getContext() || undefined, 267 | message: `Selected organization: ${selectedOrg.name}` 268 | }; 269 | } catch (error) { 270 | spinner.fail('Failed to fetch organizations'); 271 | throw error; 272 | } 273 | } 274 | 275 | /** 276 | * Execute brief selection 277 | */ 278 | private async executeSelectBrief(): Promise<void> { 279 | try { 280 | // Check authentication 281 | if (!this.authManager.isAuthenticated()) { 282 | ui.displayError('Not authenticated. Run "tm auth login" first.'); 283 | process.exit(1); 284 | } 285 | 286 | // Check if org is selected 287 | const context = this.authManager.getContext(); 288 | if (!context?.orgId) { 289 | ui.displayError( 290 | 'No organization selected. Run "tm context org" first.' 291 | ); 292 | process.exit(1); 293 | } 294 | 295 | const result = await this.selectBrief(context.orgId); 296 | this.setLastResult(result); 297 | 298 | if (!result.success) { 299 | process.exit(1); 300 | } 301 | } catch (error: any) { 302 | this.handleError(error); 303 | process.exit(1); 304 | } 305 | } 306 | 307 | /** 308 | * Select a brief within the current organization 309 | */ 310 | private async selectBrief(orgId: string): Promise<ContextResult> { 311 | const spinner = ora('Fetching briefs...').start(); 312 | 313 | try { 314 | // Fetch briefs from API 315 | const briefs = await this.authManager.getBriefs(orgId); 316 | spinner.stop(); 317 | 318 | if (briefs.length === 0) { 319 | ui.displayWarning('No briefs available in this organization'); 320 | return { 321 | success: false, 322 | action: 'select-brief', 323 | message: 'No briefs available' 324 | }; 325 | } 326 | 327 | // Prompt for selection 328 | const { selectedBrief } = await inquirer.prompt([ 329 | { 330 | type: 'list', 331 | name: 'selectedBrief', 332 | message: 'Select a brief:', 333 | choices: [ 334 | { name: '(No brief - organization level)', value: null }, 335 | ...briefs.map((brief) => ({ 336 | name: `Brief ${brief.id} (${new Date(brief.createdAt).toLocaleDateString()})`, 337 | value: brief 338 | })) 339 | ] 340 | } 341 | ]); 342 | 343 | if (selectedBrief) { 344 | // Update context with brief 345 | const briefName = `Brief ${selectedBrief.id.slice(0, 8)}`; 346 | await this.authManager.updateContext({ 347 | briefId: selectedBrief.id, 348 | briefName: briefName 349 | }); 350 | 351 | ui.displaySuccess(`Selected brief: ${briefName}`); 352 | 353 | return { 354 | success: true, 355 | action: 'select-brief', 356 | context: this.authManager.getContext() || undefined, 357 | message: `Selected brief: ${selectedBrief.name}` 358 | }; 359 | } else { 360 | // Clear brief selection 361 | await this.authManager.updateContext({ 362 | briefId: undefined, 363 | briefName: undefined 364 | }); 365 | 366 | ui.displaySuccess('Cleared brief selection (organization level)'); 367 | 368 | return { 369 | success: true, 370 | action: 'select-brief', 371 | context: this.authManager.getContext() || undefined, 372 | message: 'Cleared brief selection' 373 | }; 374 | } 375 | } catch (error) { 376 | spinner.fail('Failed to fetch briefs'); 377 | throw error; 378 | } 379 | } 380 | 381 | /** 382 | * Execute clear context 383 | */ 384 | private async executeClear(): Promise<void> { 385 | try { 386 | // Check authentication 387 | if (!this.authManager.isAuthenticated()) { 388 | ui.displayError('Not authenticated. Run "tm auth login" first.'); 389 | process.exit(1); 390 | } 391 | 392 | const result = await this.clearContext(); 393 | this.setLastResult(result); 394 | 395 | if (!result.success) { 396 | process.exit(1); 397 | } 398 | } catch (error: any) { 399 | this.handleError(error); 400 | process.exit(1); 401 | } 402 | } 403 | 404 | /** 405 | * Clear all context selections 406 | */ 407 | private async clearContext(): Promise<ContextResult> { 408 | try { 409 | await this.authManager.clearContext(); 410 | ui.displaySuccess('Context cleared'); 411 | 412 | return { 413 | success: true, 414 | action: 'clear', 415 | message: 'Context cleared' 416 | }; 417 | } catch (error) { 418 | ui.displayError(`Failed to clear context: ${(error as Error).message}`); 419 | 420 | return { 421 | success: false, 422 | action: 'clear', 423 | message: `Failed to clear context: ${(error as Error).message}` 424 | }; 425 | } 426 | } 427 | 428 | /** 429 | * Execute set context with options 430 | */ 431 | private async executeSet(options: any): Promise<void> { 432 | try { 433 | // Check authentication 434 | if (!this.authManager.isAuthenticated()) { 435 | ui.displayError('Not authenticated. Run "tm auth login" first.'); 436 | process.exit(1); 437 | } 438 | 439 | const result = await this.setContext(options); 440 | this.setLastResult(result); 441 | 442 | if (!result.success) { 443 | process.exit(1); 444 | } 445 | } catch (error: any) { 446 | this.handleError(error); 447 | process.exit(1); 448 | } 449 | } 450 | 451 | /** 452 | * Execute setting context from a brief ID or Hamster URL 453 | */ 454 | private async executeSetFromBriefInput(briefOrUrl: string): Promise<void> { 455 | let spinner: Ora | undefined; 456 | try { 457 | // Check authentication 458 | if (!this.authManager.isAuthenticated()) { 459 | ui.displayError('Not authenticated. Run "tm auth login" first.'); 460 | process.exit(1); 461 | } 462 | 463 | spinner = ora('Resolving brief...'); 464 | spinner.start(); 465 | 466 | // Extract brief ID 467 | const briefId = this.extractBriefId(briefOrUrl); 468 | if (!briefId) { 469 | spinner.fail('Could not extract a brief ID from the provided input'); 470 | ui.displayError( 471 | `Provide a valid brief ID or a Hamster brief URL, e.g. https://${process.env.TM_PUBLIC_BASE_DOMAIN}/home/hamster/briefs/<id>` 472 | ); 473 | process.exit(1); 474 | } 475 | 476 | // Fetch brief and resolve its organization 477 | const brief = await this.authManager.getBrief(briefId); 478 | if (!brief) { 479 | spinner.fail('Brief not found or you do not have access'); 480 | process.exit(1); 481 | } 482 | 483 | // Fetch org to get a friendly name (optional) 484 | let orgName: string | undefined; 485 | try { 486 | const org = await this.authManager.getOrganization(brief.accountId); 487 | orgName = org?.name; 488 | } catch { 489 | // Non-fatal if org lookup fails 490 | } 491 | 492 | // Update context: set org and brief 493 | const briefName = `Brief ${brief.id.slice(0, 8)}`; 494 | await this.authManager.updateContext({ 495 | orgId: brief.accountId, 496 | orgName, 497 | briefId: brief.id, 498 | briefName 499 | }); 500 | 501 | spinner.succeed('Context set from brief'); 502 | console.log( 503 | chalk.gray( 504 | ` Organization: ${orgName || brief.accountId}\n Brief: ${briefName}` 505 | ) 506 | ); 507 | 508 | this.setLastResult({ 509 | success: true, 510 | action: 'set', 511 | context: this.authManager.getContext() || undefined, 512 | message: 'Context set from brief' 513 | }); 514 | } catch (error: any) { 515 | try { 516 | if (spinner?.isSpinning) spinner.stop(); 517 | } catch {} 518 | this.handleError(error); 519 | process.exit(1); 520 | } 521 | } 522 | 523 | /** 524 | * Extract a brief ID from raw input (ID or Hamster URL) 525 | */ 526 | private extractBriefId(input: string): string | null { 527 | const raw = input?.trim() ?? ''; 528 | if (!raw) return null; 529 | 530 | const parseUrl = (s: string): URL | null => { 531 | try { 532 | return new URL(s); 533 | } catch {} 534 | try { 535 | return new URL(`https://${s}`); 536 | } catch {} 537 | return null; 538 | }; 539 | 540 | const fromParts = (path: string): string | null => { 541 | const parts = path.split('/').filter(Boolean); 542 | const briefsIdx = parts.lastIndexOf('briefs'); 543 | const candidate = 544 | briefsIdx >= 0 && parts.length > briefsIdx + 1 545 | ? parts[briefsIdx + 1] 546 | : parts[parts.length - 1]; 547 | return candidate?.trim() || null; 548 | }; 549 | 550 | // 1) URL (absolute or scheme‑less) 551 | const url = parseUrl(raw); 552 | if (url) { 553 | const qId = url.searchParams.get('id') || url.searchParams.get('briefId'); 554 | const candidate = (qId || fromParts(url.pathname)) ?? null; 555 | if (candidate) { 556 | // Light sanity check; let API be the final validator 557 | if (this.isLikelyId(candidate) || candidate.length >= 8) 558 | return candidate; 559 | } 560 | } 561 | 562 | // 2) Looks like a path without scheme 563 | if (raw.includes('/')) { 564 | const candidate = fromParts(raw); 565 | if (candidate && (this.isLikelyId(candidate) || candidate.length >= 8)) { 566 | return candidate; 567 | } 568 | } 569 | 570 | // 3) Fallback: raw token 571 | return raw; 572 | } 573 | 574 | /** 575 | * Heuristic to check if a string looks like a brief ID (UUID-like) 576 | */ 577 | private isLikelyId(value: string): boolean { 578 | const uuidRegex = 579 | /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; 580 | const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/i; // ULID 581 | const slugRegex = /^[A-Za-z0-9_-]{16,}$/; // general token 582 | return ( 583 | uuidRegex.test(value) || ulidRegex.test(value) || slugRegex.test(value) 584 | ); 585 | } 586 | 587 | /** 588 | * Set context directly from options 589 | */ 590 | private async setContext(options: any): Promise<ContextResult> { 591 | try { 592 | const context: Partial<UserContext> = {}; 593 | 594 | if (options.org) { 595 | context.orgId = options.org; 596 | } 597 | if (options.orgName) { 598 | context.orgName = options.orgName; 599 | } 600 | if (options.brief) { 601 | context.briefId = options.brief; 602 | } 603 | if (options.briefName) { 604 | context.briefName = options.briefName; 605 | } 606 | 607 | if (Object.keys(context).length === 0) { 608 | ui.displayWarning('No context options provided'); 609 | return { 610 | success: false, 611 | action: 'set', 612 | message: 'No context options provided' 613 | }; 614 | } 615 | 616 | await this.authManager.updateContext(context); 617 | ui.displaySuccess('Context updated'); 618 | 619 | // Display what was set 620 | if (context.orgName || context.orgId) { 621 | console.log( 622 | chalk.gray(` Organization: ${context.orgName || context.orgId}`) 623 | ); 624 | } 625 | if (context.briefName || context.briefId) { 626 | console.log( 627 | chalk.gray(` Brief: ${context.briefName || context.briefId}`) 628 | ); 629 | } 630 | 631 | return { 632 | success: true, 633 | action: 'set', 634 | context: this.authManager.getContext() || undefined, 635 | message: 'Context updated' 636 | }; 637 | } catch (error) { 638 | ui.displayError(`Failed to set context: ${(error as Error).message}`); 639 | 640 | return { 641 | success: false, 642 | action: 'set', 643 | message: `Failed to set context: ${(error as Error).message}` 644 | }; 645 | } 646 | } 647 | 648 | /** 649 | * Handle errors 650 | */ 651 | private handleError(error: any): void { 652 | if (error instanceof AuthenticationError) { 653 | console.error(chalk.red(`\n✗ ${error.message}`)); 654 | 655 | if (error.code === 'NOT_AUTHENTICATED') { 656 | ui.displayWarning('Please authenticate first: tm auth login'); 657 | } 658 | } else { 659 | const msg = error?.message ?? String(error); 660 | console.error(chalk.red(`Error: ${msg}`)); 661 | 662 | if (error.stack && process.env.DEBUG) { 663 | console.error(chalk.gray(error.stack)); 664 | } 665 | } 666 | } 667 | 668 | /** 669 | * Set the last result for programmatic access 670 | */ 671 | private setLastResult(result: ContextResult): void { 672 | this.lastResult = result; 673 | } 674 | 675 | /** 676 | * Get the last result (for programmatic usage) 677 | */ 678 | getLastResult(): ContextResult | undefined { 679 | return this.lastResult; 680 | } 681 | 682 | /** 683 | * Get current context (for programmatic usage) 684 | */ 685 | getContext(): UserContext | null { 686 | return this.authManager.getContext(); 687 | } 688 | 689 | /** 690 | * Clean up resources 691 | */ 692 | async cleanup(): Promise<void> { 693 | // No resources to clean up for context command 694 | } 695 | 696 | /** 697 | * Static method to register this command on an existing program 698 | */ 699 | static registerOn(program: Command): Command { 700 | const contextCommand = new ContextCommand(); 701 | program.addCommand(contextCommand); 702 | return contextCommand; 703 | } 704 | 705 | /** 706 | * Alternative registration that returns the command for chaining 707 | */ 708 | static register(program: Command, name?: string): ContextCommand { 709 | const contextCommand = new ContextCommand(name); 710 | program.addCommand(contextCommand); 711 | return contextCommand; 712 | } 713 | } 714 | ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/move-task-cross-tag.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | 3 | // --- Mocks --- 4 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ 5 | readJSON: jest.fn(), 6 | writeJSON: jest.fn(), 7 | log: jest.fn(), 8 | setTasksForTag: jest.fn(), 9 | truncate: jest.fn((t) => t), 10 | isSilentMode: jest.fn(() => false), 11 | traverseDependencies: jest.fn((sourceTasks, allTasks, options = {}) => { 12 | // Mock realistic dependency behavior for testing 13 | const { direction = 'forward' } = options; 14 | 15 | if (direction === 'forward') { 16 | // For forward dependencies: return tasks that the source tasks depend on 17 | const result = []; 18 | sourceTasks.forEach((task) => { 19 | if (task.dependencies && Array.isArray(task.dependencies)) { 20 | result.push(...task.dependencies); 21 | } 22 | }); 23 | return result; 24 | } else if (direction === 'reverse') { 25 | // For reverse dependencies: return tasks that depend on the source tasks 26 | const sourceIds = sourceTasks.map((t) => t.id); 27 | const normalizedSourceIds = sourceIds.map((id) => String(id)); 28 | const result = []; 29 | allTasks.forEach((task) => { 30 | if (task.dependencies && Array.isArray(task.dependencies)) { 31 | const hasDependency = task.dependencies.some((depId) => 32 | normalizedSourceIds.includes(String(depId)) 33 | ); 34 | if (hasDependency) { 35 | result.push(task.id); 36 | } 37 | } 38 | }); 39 | return result; 40 | } 41 | return []; 42 | }) 43 | })); 44 | 45 | jest.unstable_mockModule( 46 | '../../../../../scripts/modules/task-manager/generate-task-files.js', 47 | () => ({ 48 | default: jest.fn().mockResolvedValue() 49 | }) 50 | ); 51 | 52 | jest.unstable_mockModule( 53 | '../../../../../scripts/modules/task-manager.js', 54 | () => ({ 55 | isTaskDependentOn: jest.fn(() => false) 56 | }) 57 | ); 58 | 59 | jest.unstable_mockModule( 60 | '../../../../../scripts/modules/dependency-manager.js', 61 | () => ({ 62 | validateCrossTagMove: jest.fn(), 63 | findCrossTagDependencies: jest.fn(), 64 | getDependentTaskIds: jest.fn(), 65 | validateSubtaskMove: jest.fn() 66 | }) 67 | ); 68 | 69 | const { readJSON, writeJSON, log } = await import( 70 | '../../../../../scripts/modules/utils.js' 71 | ); 72 | 73 | const { 74 | validateCrossTagMove, 75 | findCrossTagDependencies, 76 | getDependentTaskIds, 77 | validateSubtaskMove 78 | } = await import('../../../../../scripts/modules/dependency-manager.js'); 79 | 80 | const { moveTasksBetweenTags, getAllTasksWithTags } = await import( 81 | '../../../../../scripts/modules/task-manager/move-task.js' 82 | ); 83 | 84 | describe('Cross-Tag Task Movement', () => { 85 | let mockRawData; 86 | let mockTasksPath; 87 | let mockContext; 88 | 89 | beforeEach(() => { 90 | jest.clearAllMocks(); 91 | 92 | // Setup mock data 93 | mockRawData = { 94 | backlog: { 95 | tasks: [ 96 | { id: 1, title: 'Task 1', dependencies: [2] }, 97 | { id: 2, title: 'Task 2', dependencies: [] }, 98 | { id: 3, title: 'Task 3', dependencies: [1] } 99 | ] 100 | }, 101 | 'in-progress': { 102 | tasks: [{ id: 4, title: 'Task 4', dependencies: [] }] 103 | }, 104 | done: { 105 | tasks: [{ id: 5, title: 'Task 5', dependencies: [4] }] 106 | } 107 | }; 108 | 109 | mockTasksPath = '/test/path/tasks.json'; 110 | mockContext = { projectRoot: '/test/project' }; 111 | 112 | // Mock readJSON to return our test data 113 | readJSON.mockImplementation((path, projectRoot, tag) => { 114 | return { ...mockRawData[tag], tag, _rawTaggedData: mockRawData }; 115 | }); 116 | 117 | writeJSON.mockResolvedValue(); 118 | log.mockImplementation(() => {}); 119 | }); 120 | 121 | afterEach(() => { 122 | jest.clearAllMocks(); 123 | }); 124 | 125 | describe('getAllTasksWithTags', () => { 126 | it('should return all tasks with tag information', () => { 127 | const allTasks = getAllTasksWithTags(mockRawData); 128 | 129 | expect(allTasks).toHaveLength(5); 130 | expect(allTasks.find((t) => t.id === 1).tag).toBe('backlog'); 131 | expect(allTasks.find((t) => t.id === 4).tag).toBe('in-progress'); 132 | expect(allTasks.find((t) => t.id === 5).tag).toBe('done'); 133 | }); 134 | }); 135 | 136 | describe('validateCrossTagMove', () => { 137 | it('should allow move when no dependencies exist', () => { 138 | const task = { id: 2, dependencies: [] }; 139 | const allTasks = getAllTasksWithTags(mockRawData); 140 | 141 | validateCrossTagMove.mockReturnValue({ canMove: true, conflicts: [] }); 142 | const result = validateCrossTagMove( 143 | task, 144 | 'backlog', 145 | 'in-progress', 146 | allTasks 147 | ); 148 | 149 | expect(result.canMove).toBe(true); 150 | expect(result.conflicts).toHaveLength(0); 151 | }); 152 | 153 | it('should block move when cross-tag dependencies exist', () => { 154 | const task = { id: 1, dependencies: [2] }; 155 | const allTasks = getAllTasksWithTags(mockRawData); 156 | 157 | validateCrossTagMove.mockReturnValue({ 158 | canMove: false, 159 | conflicts: [{ taskId: 1, dependencyId: 2, dependencyTag: 'backlog' }] 160 | }); 161 | const result = validateCrossTagMove( 162 | task, 163 | 'backlog', 164 | 'in-progress', 165 | allTasks 166 | ); 167 | 168 | expect(result.canMove).toBe(false); 169 | expect(result.conflicts).toHaveLength(1); 170 | expect(result.conflicts[0].dependencyId).toBe(2); 171 | }); 172 | }); 173 | 174 | describe('findCrossTagDependencies', () => { 175 | it('should find cross-tag dependencies for multiple tasks', () => { 176 | const sourceTasks = [ 177 | { id: 1, dependencies: [2] }, 178 | { id: 3, dependencies: [1] } 179 | ]; 180 | const allTasks = getAllTasksWithTags(mockRawData); 181 | 182 | findCrossTagDependencies.mockReturnValue([ 183 | { taskId: 1, dependencyId: 2, dependencyTag: 'backlog' }, 184 | { taskId: 3, dependencyId: 1, dependencyTag: 'backlog' } 185 | ]); 186 | const conflicts = findCrossTagDependencies( 187 | sourceTasks, 188 | 'backlog', 189 | 'in-progress', 190 | allTasks 191 | ); 192 | 193 | expect(conflicts).toHaveLength(2); 194 | expect( 195 | conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2) 196 | ).toBe(true); 197 | expect( 198 | conflicts.some((c) => c.taskId === 3 && c.dependencyId === 1) 199 | ).toBe(true); 200 | }); 201 | }); 202 | 203 | describe('getDependentTaskIds', () => { 204 | it('should return dependent task IDs', () => { 205 | const sourceTasks = [{ id: 1, dependencies: [2] }]; 206 | const crossTagDependencies = [ 207 | { taskId: 1, dependencyId: 2, dependencyTag: 'backlog' } 208 | ]; 209 | const allTasks = getAllTasksWithTags(mockRawData); 210 | 211 | getDependentTaskIds.mockReturnValue([2]); 212 | const dependentTaskIds = getDependentTaskIds( 213 | sourceTasks, 214 | crossTagDependencies, 215 | allTasks 216 | ); 217 | 218 | expect(dependentTaskIds).toContain(2); 219 | }); 220 | }); 221 | 222 | // New test: ensure with-dependencies only traverses tasks from the source tag 223 | it('should scope dependency traversal to source tag when using --with-dependencies', async () => { 224 | findCrossTagDependencies.mockReturnValue([]); 225 | validateSubtaskMove.mockImplementation(() => {}); 226 | 227 | const result = await moveTasksBetweenTags( 228 | mockTasksPath, 229 | [1], // backlog:1 depends on backlog:2 230 | 'backlog', 231 | 'in-progress', 232 | { withDependencies: true }, 233 | mockContext 234 | ); 235 | 236 | // Write should include backlog:2 moved, and must NOT traverse or fetch dependencies from the target tag 237 | expect(writeJSON).toHaveBeenCalledWith( 238 | mockTasksPath, 239 | expect.objectContaining({ 240 | 'in-progress': expect.objectContaining({ 241 | tasks: expect.arrayContaining([ 242 | expect.objectContaining({ id: 1 }), 243 | expect.objectContaining({ id: 2 }) // the backlog:2 now moved 244 | // ensure existing in-progress:2 remains (by id) but we don't double-add or fetch deps from it 245 | ]) 246 | }) 247 | }), 248 | mockContext.projectRoot, 249 | null 250 | ); 251 | }); 252 | 253 | describe('moveTasksBetweenTags', () => { 254 | it('should move tasks without dependencies successfully', async () => { 255 | // Mock the dependency functions to return no conflicts 256 | findCrossTagDependencies.mockReturnValue([]); 257 | validateSubtaskMove.mockImplementation(() => {}); 258 | 259 | const result = await moveTasksBetweenTags( 260 | mockTasksPath, 261 | [2], 262 | 'backlog', 263 | 'in-progress', 264 | {}, 265 | mockContext 266 | ); 267 | 268 | expect(result.message).toContain('Successfully moved 1 tasks'); 269 | expect(writeJSON).toHaveBeenCalledWith( 270 | mockTasksPath, 271 | expect.any(Object), 272 | mockContext.projectRoot, 273 | null 274 | ); 275 | }); 276 | 277 | it('should throw error for cross-tag dependencies by default', async () => { 278 | const mockDependency = { 279 | taskId: 1, 280 | dependencyId: 2, 281 | dependencyTag: 'backlog' 282 | }; 283 | findCrossTagDependencies.mockReturnValue([mockDependency]); 284 | validateSubtaskMove.mockImplementation(() => {}); 285 | 286 | await expect( 287 | moveTasksBetweenTags( 288 | mockTasksPath, 289 | [1], 290 | 'backlog', 291 | 'in-progress', 292 | {}, 293 | mockContext 294 | ) 295 | ).rejects.toThrow( 296 | 'Cannot move tasks: 1 cross-tag dependency conflicts found' 297 | ); 298 | 299 | expect(writeJSON).not.toHaveBeenCalled(); 300 | }); 301 | 302 | it('should move with dependencies when --with-dependencies is used', async () => { 303 | const mockDependency = { 304 | taskId: 1, 305 | dependencyId: 2, 306 | dependencyTag: 'backlog' 307 | }; 308 | findCrossTagDependencies.mockReturnValue([mockDependency]); 309 | getDependentTaskIds.mockReturnValue([2]); 310 | validateSubtaskMove.mockImplementation(() => {}); 311 | 312 | const result = await moveTasksBetweenTags( 313 | mockTasksPath, 314 | [1], 315 | 'backlog', 316 | 'in-progress', 317 | { withDependencies: true }, 318 | mockContext 319 | ); 320 | 321 | expect(result.message).toContain('Successfully moved 2 tasks'); 322 | expect(writeJSON).toHaveBeenCalledWith( 323 | mockTasksPath, 324 | expect.objectContaining({ 325 | backlog: expect.objectContaining({ 326 | tasks: expect.arrayContaining([ 327 | expect.objectContaining({ 328 | id: 3, 329 | title: 'Task 3', 330 | dependencies: [1] 331 | }) 332 | ]) 333 | }), 334 | 'in-progress': expect.objectContaining({ 335 | tasks: expect.arrayContaining([ 336 | expect.objectContaining({ 337 | id: 4, 338 | title: 'Task 4', 339 | dependencies: [] 340 | }), 341 | expect.objectContaining({ 342 | id: 1, 343 | title: 'Task 1', 344 | dependencies: [2], 345 | metadata: expect.objectContaining({ 346 | moveHistory: expect.arrayContaining([ 347 | expect.objectContaining({ 348 | fromTag: 'backlog', 349 | toTag: 'in-progress', 350 | timestamp: expect.any(String) 351 | }) 352 | ]) 353 | }) 354 | }), 355 | expect.objectContaining({ 356 | id: 2, 357 | title: 'Task 2', 358 | dependencies: [], 359 | metadata: expect.objectContaining({ 360 | moveHistory: expect.arrayContaining([ 361 | expect.objectContaining({ 362 | fromTag: 'backlog', 363 | toTag: 'in-progress', 364 | timestamp: expect.any(String) 365 | }) 366 | ]) 367 | }) 368 | }) 369 | ]) 370 | }), 371 | done: expect.objectContaining({ 372 | tasks: expect.arrayContaining([ 373 | expect.objectContaining({ 374 | id: 5, 375 | title: 'Task 5', 376 | dependencies: [4] 377 | }) 378 | ]) 379 | }) 380 | }), 381 | mockContext.projectRoot, 382 | null 383 | ); 384 | }); 385 | 386 | it('should break dependencies when --ignore-dependencies is used', async () => { 387 | const mockDependency = { 388 | taskId: 1, 389 | dependencyId: 2, 390 | dependencyTag: 'backlog' 391 | }; 392 | findCrossTagDependencies.mockReturnValue([mockDependency]); 393 | validateSubtaskMove.mockImplementation(() => {}); 394 | 395 | const result = await moveTasksBetweenTags( 396 | mockTasksPath, 397 | [2], 398 | 'backlog', 399 | 'in-progress', 400 | { ignoreDependencies: true }, 401 | mockContext 402 | ); 403 | 404 | expect(result.message).toContain('Successfully moved 1 tasks'); 405 | expect(writeJSON).toHaveBeenCalledWith( 406 | mockTasksPath, 407 | expect.objectContaining({ 408 | backlog: expect.objectContaining({ 409 | tasks: expect.arrayContaining([ 410 | expect.objectContaining({ 411 | id: 1, 412 | title: 'Task 1', 413 | dependencies: [2] // Dependencies not actually removed in current implementation 414 | }), 415 | expect.objectContaining({ 416 | id: 3, 417 | title: 'Task 3', 418 | dependencies: [1] 419 | }) 420 | ]) 421 | }), 422 | 'in-progress': expect.objectContaining({ 423 | tasks: expect.arrayContaining([ 424 | expect.objectContaining({ 425 | id: 4, 426 | title: 'Task 4', 427 | dependencies: [] 428 | }), 429 | expect.objectContaining({ 430 | id: 2, 431 | title: 'Task 2', 432 | dependencies: [], 433 | metadata: expect.objectContaining({ 434 | moveHistory: expect.arrayContaining([ 435 | expect.objectContaining({ 436 | fromTag: 'backlog', 437 | toTag: 'in-progress', 438 | timestamp: expect.any(String) 439 | }) 440 | ]) 441 | }) 442 | }) 443 | ]) 444 | }), 445 | done: expect.objectContaining({ 446 | tasks: expect.arrayContaining([ 447 | expect.objectContaining({ 448 | id: 5, 449 | title: 'Task 5', 450 | dependencies: [4] 451 | }) 452 | ]) 453 | }) 454 | }), 455 | mockContext.projectRoot, 456 | null 457 | ); 458 | }); 459 | 460 | it('should create target tag if it does not exist', async () => { 461 | findCrossTagDependencies.mockReturnValue([]); 462 | validateSubtaskMove.mockImplementation(() => {}); 463 | 464 | const result = await moveTasksBetweenTags( 465 | mockTasksPath, 466 | [2], 467 | 'backlog', 468 | 'new-tag', 469 | {}, 470 | mockContext 471 | ); 472 | 473 | expect(result.message).toContain('Successfully moved 1 tasks'); 474 | expect(result.message).toContain('new-tag'); 475 | expect(writeJSON).toHaveBeenCalledWith( 476 | mockTasksPath, 477 | expect.objectContaining({ 478 | backlog: expect.objectContaining({ 479 | tasks: expect.arrayContaining([ 480 | expect.objectContaining({ 481 | id: 1, 482 | title: 'Task 1', 483 | dependencies: [2] 484 | }), 485 | expect.objectContaining({ 486 | id: 3, 487 | title: 'Task 3', 488 | dependencies: [1] 489 | }) 490 | ]) 491 | }), 492 | 'new-tag': expect.objectContaining({ 493 | tasks: expect.arrayContaining([ 494 | expect.objectContaining({ 495 | id: 2, 496 | title: 'Task 2', 497 | dependencies: [], 498 | metadata: expect.objectContaining({ 499 | moveHistory: expect.arrayContaining([ 500 | expect.objectContaining({ 501 | fromTag: 'backlog', 502 | toTag: 'new-tag', 503 | timestamp: expect.any(String) 504 | }) 505 | ]) 506 | }) 507 | }) 508 | ]) 509 | }), 510 | 'in-progress': expect.objectContaining({ 511 | tasks: expect.arrayContaining([ 512 | expect.objectContaining({ 513 | id: 4, 514 | title: 'Task 4', 515 | dependencies: [] 516 | }) 517 | ]) 518 | }), 519 | done: expect.objectContaining({ 520 | tasks: expect.arrayContaining([ 521 | expect.objectContaining({ 522 | id: 5, 523 | title: 'Task 5', 524 | dependencies: [4] 525 | }) 526 | ]) 527 | }) 528 | }), 529 | mockContext.projectRoot, 530 | null 531 | ); 532 | }); 533 | 534 | it('should throw error for subtask movement', async () => { 535 | const subtaskError = 'Cannot move subtask 1.2 directly between tags'; 536 | validateSubtaskMove.mockImplementation(() => { 537 | throw new Error(subtaskError); 538 | }); 539 | 540 | await expect( 541 | moveTasksBetweenTags( 542 | mockTasksPath, 543 | ['1.2'], 544 | 'backlog', 545 | 'in-progress', 546 | {}, 547 | mockContext 548 | ) 549 | ).rejects.toThrow(subtaskError); 550 | 551 | expect(writeJSON).not.toHaveBeenCalled(); 552 | }); 553 | 554 | it('should throw error for invalid task IDs', async () => { 555 | findCrossTagDependencies.mockReturnValue([]); 556 | validateSubtaskMove.mockImplementation(() => {}); 557 | 558 | await expect( 559 | moveTasksBetweenTags( 560 | mockTasksPath, 561 | [999], // Non-existent task 562 | 'backlog', 563 | 'in-progress', 564 | {}, 565 | mockContext 566 | ) 567 | ).rejects.toThrow('Task 999 not found in source tag "backlog"'); 568 | 569 | expect(writeJSON).not.toHaveBeenCalled(); 570 | }); 571 | 572 | it('should throw error for invalid source tag', async () => { 573 | findCrossTagDependencies.mockReturnValue([]); 574 | validateSubtaskMove.mockImplementation(() => {}); 575 | 576 | await expect( 577 | moveTasksBetweenTags( 578 | mockTasksPath, 579 | [1], 580 | 'non-existent-tag', 581 | 'in-progress', 582 | {}, 583 | mockContext 584 | ) 585 | ).rejects.toThrow('Source tag "non-existent-tag" not found or invalid'); 586 | 587 | expect(writeJSON).not.toHaveBeenCalled(); 588 | }); 589 | 590 | it('should handle string dependencies correctly during cross-tag move', async () => { 591 | // Setup mock data with string dependencies 592 | mockRawData = { 593 | backlog: { 594 | tasks: [ 595 | { id: 1, title: 'Task 1', dependencies: ['2'] }, // String dependency 596 | { id: 2, title: 'Task 2', dependencies: [] }, 597 | { id: 3, title: 'Task 3', dependencies: ['1'] } // String dependency 598 | ] 599 | }, 600 | 'in-progress': { 601 | tasks: [{ id: 4, title: 'Task 4', dependencies: [] }] 602 | } 603 | }; 604 | 605 | // Mock readJSON to return our test data 606 | readJSON.mockImplementation((path, projectRoot, tag) => { 607 | return { ...mockRawData[tag], tag, _rawTaggedData: mockRawData }; 608 | }); 609 | 610 | findCrossTagDependencies.mockReturnValue([]); 611 | validateSubtaskMove.mockImplementation(() => {}); 612 | 613 | const result = await moveTasksBetweenTags( 614 | mockTasksPath, 615 | ['1'], // String task ID 616 | 'backlog', 617 | 'in-progress', 618 | {}, 619 | mockContext 620 | ); 621 | 622 | expect(result.message).toContain('Successfully moved 1 tasks'); 623 | expect(writeJSON).toHaveBeenCalledWith( 624 | mockTasksPath, 625 | expect.objectContaining({ 626 | backlog: expect.objectContaining({ 627 | tasks: expect.arrayContaining([ 628 | expect.objectContaining({ 629 | id: 2, 630 | title: 'Task 2', 631 | dependencies: [] 632 | }), 633 | expect.objectContaining({ 634 | id: 3, 635 | title: 'Task 3', 636 | dependencies: ['1'] // Should remain as string 637 | }) 638 | ]) 639 | }), 640 | 'in-progress': expect.objectContaining({ 641 | tasks: expect.arrayContaining([ 642 | expect.objectContaining({ 643 | id: 1, 644 | title: 'Task 1', 645 | dependencies: ['2'], // Should remain as string 646 | metadata: expect.objectContaining({ 647 | moveHistory: expect.arrayContaining([ 648 | expect.objectContaining({ 649 | fromTag: 'backlog', 650 | toTag: 'in-progress', 651 | timestamp: expect.any(String) 652 | }) 653 | ]) 654 | }) 655 | }) 656 | ]) 657 | }) 658 | }), 659 | mockContext.projectRoot, 660 | null 661 | ); 662 | }); 663 | }); 664 | }); 665 | ``` -------------------------------------------------------------------------------- /tests/integration/move-task-simple.integration.test.js: -------------------------------------------------------------------------------- ```javascript 1 | import { jest } from '@jest/globals'; 2 | import path from 'path'; 3 | import mockFs from 'mock-fs'; 4 | import fs from 'fs'; 5 | import { fileURLToPath } from 'url'; 6 | 7 | // Import the actual move task functionality 8 | import moveTask, { 9 | moveTasksBetweenTags 10 | } from '../../scripts/modules/task-manager/move-task.js'; 11 | import { readJSON, writeJSON } from '../../scripts/modules/utils.js'; 12 | 13 | // Mock console to avoid conflicts with mock-fs 14 | const originalConsole = { ...console }; 15 | beforeAll(() => { 16 | global.console = { 17 | ...console, 18 | log: jest.fn(), 19 | error: jest.fn(), 20 | warn: jest.fn(), 21 | info: jest.fn() 22 | }; 23 | }); 24 | 25 | afterAll(() => { 26 | global.console = originalConsole; 27 | }); 28 | 29 | // Get __dirname equivalent for ES modules 30 | const __filename = fileURLToPath(import.meta.url); 31 | const __dirname = path.dirname(__filename); 32 | 33 | describe('Cross-Tag Task Movement Simple Integration Tests', () => { 34 | const testDataDir = path.join(__dirname, 'fixtures'); 35 | const testTasksPath = path.join(testDataDir, 'tasks.json'); 36 | 37 | // Test data structure with proper tagged format 38 | const testData = { 39 | backlog: { 40 | tasks: [ 41 | { id: 1, title: 'Task 1', dependencies: [], status: 'pending' }, 42 | { id: 2, title: 'Task 2', dependencies: [], status: 'pending' } 43 | ] 44 | }, 45 | 'in-progress': { 46 | tasks: [ 47 | { id: 3, title: 'Task 3', dependencies: [], status: 'in-progress' } 48 | ] 49 | } 50 | }; 51 | 52 | beforeEach(() => { 53 | // Set up mock file system with test data 54 | mockFs({ 55 | [testDataDir]: { 56 | 'tasks.json': JSON.stringify(testData, null, 2) 57 | } 58 | }); 59 | }); 60 | 61 | afterEach(() => { 62 | // Clean up mock file system 63 | mockFs.restore(); 64 | }); 65 | 66 | describe('Real Module Integration Tests', () => { 67 | it('should move task within same tag using actual moveTask function', async () => { 68 | // Test moving Task 1 from position 1 to position 5 within backlog tag 69 | const result = await moveTask( 70 | testTasksPath, 71 | '1', 72 | '5', 73 | false, // Don't generate files for this test 74 | { tag: 'backlog' } 75 | ); 76 | 77 | // Verify the move operation was successful 78 | expect(result).toBeDefined(); 79 | expect(result.message).toContain('Moved task 1 to new ID 5'); 80 | 81 | // Read the updated data to verify the move actually happened 82 | const updatedData = readJSON(testTasksPath, null, 'backlog'); 83 | const rawData = updatedData._rawTaggedData || updatedData; 84 | const backlogTasks = rawData.backlog.tasks; 85 | 86 | // Verify Task 1 is no longer at position 1 87 | const taskAtPosition1 = backlogTasks.find((t) => t.id === 1); 88 | expect(taskAtPosition1).toBeUndefined(); 89 | 90 | // Verify Task 1 is now at position 5 91 | const taskAtPosition5 = backlogTasks.find((t) => t.id === 5); 92 | expect(taskAtPosition5).toBeDefined(); 93 | expect(taskAtPosition5.title).toBe('Task 1'); 94 | expect(taskAtPosition5.status).toBe('pending'); 95 | }); 96 | 97 | it('should move tasks between tags using moveTasksBetweenTags function', async () => { 98 | // Test moving Task 1 from backlog to in-progress tag 99 | const result = await moveTasksBetweenTags( 100 | testTasksPath, 101 | ['1'], // Task IDs to move (as strings) 102 | 'backlog', // Source tag 103 | 'in-progress', // Target tag 104 | { withDependencies: false, ignoreDependencies: false }, 105 | { projectRoot: testDataDir } 106 | ); 107 | 108 | // Verify the cross-tag move operation was successful 109 | expect(result).toBeDefined(); 110 | expect(result.message).toContain( 111 | 'Successfully moved 1 tasks from "backlog" to "in-progress"' 112 | ); 113 | expect(result.movedTasks).toHaveLength(1); 114 | expect(result.movedTasks[0].id).toBe('1'); 115 | expect(result.movedTasks[0].fromTag).toBe('backlog'); 116 | expect(result.movedTasks[0].toTag).toBe('in-progress'); 117 | 118 | // Read the updated data to verify the move actually happened 119 | const updatedData = readJSON(testTasksPath, null, 'backlog'); 120 | // readJSON returns resolved data, so we need to access the raw tagged data 121 | const rawData = updatedData._rawTaggedData || updatedData; 122 | const backlogTasks = rawData.backlog?.tasks || []; 123 | const inProgressTasks = rawData['in-progress']?.tasks || []; 124 | 125 | // Verify Task 1 is no longer in backlog 126 | const taskInBacklog = backlogTasks.find((t) => t.id === 1); 127 | expect(taskInBacklog).toBeUndefined(); 128 | 129 | // Verify Task 1 is now in in-progress 130 | const taskInProgress = inProgressTasks.find((t) => t.id === 1); 131 | expect(taskInProgress).toBeDefined(); 132 | expect(taskInProgress.title).toBe('Task 1'); 133 | expect(taskInProgress.status).toBe('pending'); 134 | }); 135 | 136 | it('should handle subtask movement restrictions', async () => { 137 | // Create data with subtasks 138 | const dataWithSubtasks = { 139 | backlog: { 140 | tasks: [ 141 | { 142 | id: 1, 143 | title: 'Task 1', 144 | dependencies: [], 145 | status: 'pending', 146 | subtasks: [ 147 | { id: '1.1', title: 'Subtask 1.1', status: 'pending' }, 148 | { id: '1.2', title: 'Subtask 1.2', status: 'pending' } 149 | ] 150 | } 151 | ] 152 | }, 153 | 'in-progress': { 154 | tasks: [ 155 | { id: 2, title: 'Task 2', dependencies: [], status: 'in-progress' } 156 | ] 157 | } 158 | }; 159 | 160 | // Write subtask data to mock file system 161 | mockFs({ 162 | [testDataDir]: { 163 | 'tasks.json': JSON.stringify(dataWithSubtasks, null, 2) 164 | } 165 | }); 166 | 167 | // Try to move a subtask directly - this should actually work (converts subtask to task) 168 | const result = await moveTask( 169 | testTasksPath, 170 | '1.1', // Subtask ID 171 | '5', // New task ID 172 | false, 173 | { tag: 'backlog' } 174 | ); 175 | 176 | // Verify the subtask was converted to a task 177 | expect(result).toBeDefined(); 178 | expect(result.message).toContain('Converted subtask 1.1 to task 5'); 179 | 180 | // Verify the subtask was removed from the parent and converted to a standalone task 181 | const updatedData = readJSON(testTasksPath, null, 'backlog'); 182 | const rawData = updatedData._rawTaggedData || updatedData; 183 | const task1 = rawData.backlog?.tasks?.find((t) => t.id === 1); 184 | const newTask5 = rawData.backlog?.tasks?.find((t) => t.id === 5); 185 | 186 | expect(task1).toBeDefined(); 187 | expect(task1.subtasks).toHaveLength(1); // Only 1.2 remains 188 | expect(task1.subtasks[0].id).toBe(2); 189 | 190 | expect(newTask5).toBeDefined(); 191 | expect(newTask5.title).toBe('Subtask 1.1'); 192 | expect(newTask5.status).toBe('pending'); 193 | }); 194 | 195 | it('should handle missing source tag errors', async () => { 196 | // Try to move from a non-existent tag 197 | await expect( 198 | moveTasksBetweenTags( 199 | testTasksPath, 200 | ['1'], 201 | 'non-existent-tag', // Source tag doesn't exist 202 | 'in-progress', 203 | { withDependencies: false, ignoreDependencies: false }, 204 | { projectRoot: testDataDir } 205 | ) 206 | ).rejects.toThrow(); 207 | }); 208 | 209 | it('should handle missing task ID errors', async () => { 210 | // Try to move a non-existent task 211 | await expect( 212 | moveTask( 213 | testTasksPath, 214 | '999', // Non-existent task ID 215 | '5', 216 | false, 217 | { tag: 'backlog' } 218 | ) 219 | ).rejects.toThrow(); 220 | }); 221 | 222 | it('should handle ignoreDependencies option correctly', async () => { 223 | // Create data with dependencies 224 | const dataWithDependencies = { 225 | backlog: { 226 | tasks: [ 227 | { id: 1, title: 'Task 1', dependencies: [2], status: 'pending' }, 228 | { id: 2, title: 'Task 2', dependencies: [], status: 'pending' } 229 | ] 230 | }, 231 | 'in-progress': { 232 | tasks: [ 233 | { id: 3, title: 'Task 3', dependencies: [], status: 'in-progress' } 234 | ] 235 | } 236 | }; 237 | 238 | // Write dependency data to mock file system 239 | mockFs({ 240 | [testDataDir]: { 241 | 'tasks.json': JSON.stringify(dataWithDependencies, null, 2) 242 | } 243 | }); 244 | 245 | // Move Task 1 while ignoring its dependencies 246 | const result = await moveTasksBetweenTags( 247 | testTasksPath, 248 | ['1'], // Only Task 1 249 | 'backlog', 250 | 'in-progress', 251 | { withDependencies: false, ignoreDependencies: true }, 252 | { projectRoot: testDataDir } 253 | ); 254 | 255 | expect(result).toBeDefined(); 256 | expect(result.movedTasks).toHaveLength(1); 257 | 258 | // Verify Task 1 moved but Task 2 stayed 259 | const updatedData = readJSON(testTasksPath, null, 'backlog'); 260 | const rawData = updatedData._rawTaggedData || updatedData; 261 | expect(rawData.backlog.tasks).toHaveLength(1); // Task 2 remains 262 | expect(rawData['in-progress'].tasks).toHaveLength(2); // Task 3 + Task 1 263 | 264 | // Verify Task 1 has no dependencies (they were ignored) 265 | const movedTask = rawData['in-progress'].tasks.find((t) => t.id === 1); 266 | expect(movedTask.dependencies).toEqual([]); 267 | }); 268 | }); 269 | 270 | describe('Complex Dependency Scenarios', () => { 271 | beforeAll(() => { 272 | // Document the mock-fs limitation for complex dependency scenarios 273 | console.warn( 274 | '⚠️ Complex dependency tests are skipped due to mock-fs limitations. ' + 275 | 'These tests require real filesystem operations for proper dependency resolution. ' + 276 | 'Consider using real temporary filesystem setup for these scenarios.' 277 | ); 278 | }); 279 | 280 | it.skip('should handle dependency conflicts during cross-tag moves', async () => { 281 | // For now, skip this test as the mock setup is not working correctly 282 | // TODO: Fix mock-fs setup for complex dependency scenarios 283 | }); 284 | 285 | it.skip('should handle withDependencies option correctly', async () => { 286 | // For now, skip this test as the mock setup is not working correctly 287 | // TODO: Fix mock-fs setup for complex dependency scenarios 288 | }); 289 | }); 290 | 291 | describe('Complex Dependency Integration Tests with Mock-fs', () => { 292 | const complexTestData = { 293 | backlog: { 294 | tasks: [ 295 | { id: 1, title: 'Task 1', dependencies: [2, 3], status: 'pending' }, 296 | { id: 2, title: 'Task 2', dependencies: [4], status: 'pending' }, 297 | { id: 3, title: 'Task 3', dependencies: [], status: 'pending' }, 298 | { id: 4, title: 'Task 4', dependencies: [], status: 'pending' } 299 | ] 300 | }, 301 | 'in-progress': { 302 | tasks: [ 303 | { id: 5, title: 'Task 5', dependencies: [], status: 'in-progress' } 304 | ] 305 | } 306 | }; 307 | 308 | beforeEach(() => { 309 | // Set up mock file system with complex dependency data 310 | mockFs({ 311 | [testDataDir]: { 312 | 'tasks.json': JSON.stringify(complexTestData, null, 2) 313 | } 314 | }); 315 | }); 316 | 317 | afterEach(() => { 318 | // Clean up mock file system 319 | mockFs.restore(); 320 | }); 321 | 322 | it('should handle dependency conflicts during cross-tag moves using actual move functions', async () => { 323 | // Test moving Task 1 which has dependencies on Tasks 2 and 3 324 | // This should fail because Task 1 depends on Tasks 2 and 3 which are in the same tag 325 | await expect( 326 | moveTasksBetweenTags( 327 | testTasksPath, 328 | ['1'], // Task 1 with dependencies 329 | 'backlog', 330 | 'in-progress', 331 | { withDependencies: false, ignoreDependencies: false }, 332 | { projectRoot: testDataDir } 333 | ) 334 | ).rejects.toThrow( 335 | 'Cannot move tasks: 2 cross-tag dependency conflicts found' 336 | ); 337 | }); 338 | 339 | it('should handle withDependencies option correctly using actual move functions', async () => { 340 | // Test moving Task 1 with its dependencies (Tasks 2 and 3) 341 | // Task 2 also depends on Task 4, so all 4 tasks should move 342 | const result = await moveTasksBetweenTags( 343 | testTasksPath, 344 | ['1'], // Task 1 345 | 'backlog', 346 | 'in-progress', 347 | { withDependencies: true, ignoreDependencies: false }, 348 | { projectRoot: testDataDir } 349 | ); 350 | 351 | // Verify the move operation was successful 352 | expect(result).toBeDefined(); 353 | expect(result.message).toContain( 354 | 'Successfully moved 4 tasks from "backlog" to "in-progress"' 355 | ); 356 | expect(result.movedTasks).toHaveLength(4); // Task 1 + Tasks 2, 3, 4 357 | 358 | // Read the updated data to verify all dependent tasks moved 359 | const updatedData = readJSON(testTasksPath, null, 'backlog'); 360 | const rawData = updatedData._rawTaggedData || updatedData; 361 | 362 | // Verify all tasks moved from backlog 363 | expect(rawData.backlog?.tasks || []).toHaveLength(0); // All tasks moved 364 | 365 | // Verify all tasks are now in in-progress 366 | expect(rawData['in-progress']?.tasks || []).toHaveLength(5); // Task 5 + Tasks 1, 2, 3, 4 367 | 368 | // Verify dependency relationships are preserved 369 | const task1 = rawData['in-progress']?.tasks?.find((t) => t.id === 1); 370 | const task2 = rawData['in-progress']?.tasks?.find((t) => t.id === 2); 371 | const task3 = rawData['in-progress']?.tasks?.find((t) => t.id === 3); 372 | const task4 = rawData['in-progress']?.tasks?.find((t) => t.id === 4); 373 | 374 | expect(task1?.dependencies).toEqual([2, 3]); 375 | expect(task2?.dependencies).toEqual([4]); 376 | expect(task3?.dependencies).toEqual([]); 377 | expect(task4?.dependencies).toEqual([]); 378 | }); 379 | 380 | it('should handle circular dependency detection using actual move functions', async () => { 381 | // Create data with circular dependencies 382 | const circularData = { 383 | backlog: { 384 | tasks: [ 385 | { id: 1, title: 'Task 1', dependencies: [2], status: 'pending' }, 386 | { id: 2, title: 'Task 2', dependencies: [3], status: 'pending' }, 387 | { id: 3, title: 'Task 3', dependencies: [1], status: 'pending' } // Circular dependency 388 | ] 389 | }, 390 | 'in-progress': { 391 | tasks: [ 392 | { id: 4, title: 'Task 4', dependencies: [], status: 'in-progress' } 393 | ] 394 | } 395 | }; 396 | 397 | // Set up mock file system with circular dependency data 398 | mockFs({ 399 | [testDataDir]: { 400 | 'tasks.json': JSON.stringify(circularData, null, 2) 401 | } 402 | }); 403 | 404 | // Attempt to move Task 1 with dependencies should fail due to circular dependency 405 | await expect( 406 | moveTasksBetweenTags( 407 | testTasksPath, 408 | ['1'], 409 | 'backlog', 410 | 'in-progress', 411 | { withDependencies: true, ignoreDependencies: false }, 412 | { projectRoot: testDataDir } 413 | ) 414 | ).rejects.toThrow(); 415 | }); 416 | 417 | it('should handle nested dependency chains using actual move functions', async () => { 418 | // Create data with nested dependency chains 419 | const nestedData = { 420 | backlog: { 421 | tasks: [ 422 | { id: 1, title: 'Task 1', dependencies: [2], status: 'pending' }, 423 | { id: 2, title: 'Task 2', dependencies: [3], status: 'pending' }, 424 | { id: 3, title: 'Task 3', dependencies: [4], status: 'pending' }, 425 | { id: 4, title: 'Task 4', dependencies: [], status: 'pending' } 426 | ] 427 | }, 428 | 'in-progress': { 429 | tasks: [ 430 | { id: 5, title: 'Task 5', dependencies: [], status: 'in-progress' } 431 | ] 432 | } 433 | }; 434 | 435 | // Set up mock file system with nested dependency data 436 | mockFs({ 437 | [testDataDir]: { 438 | 'tasks.json': JSON.stringify(nestedData, null, 2) 439 | } 440 | }); 441 | 442 | // Test moving Task 1 with all its nested dependencies 443 | const result = await moveTasksBetweenTags( 444 | testTasksPath, 445 | ['1'], // Task 1 446 | 'backlog', 447 | 'in-progress', 448 | { withDependencies: true, ignoreDependencies: false }, 449 | { projectRoot: testDataDir } 450 | ); 451 | 452 | // Verify the move operation was successful 453 | expect(result).toBeDefined(); 454 | expect(result.message).toContain( 455 | 'Successfully moved 4 tasks from "backlog" to "in-progress"' 456 | ); 457 | expect(result.movedTasks).toHaveLength(4); // Tasks 1, 2, 3, 4 458 | 459 | // Read the updated data to verify all tasks moved 460 | const updatedData = readJSON(testTasksPath, null, 'backlog'); 461 | const rawData = updatedData._rawTaggedData || updatedData; 462 | 463 | // Verify all tasks moved from backlog 464 | expect(rawData.backlog?.tasks || []).toHaveLength(0); // All tasks moved 465 | 466 | // Verify all tasks are now in in-progress 467 | expect(rawData['in-progress']?.tasks || []).toHaveLength(5); // Task 5 + Tasks 1, 2, 3, 4 468 | 469 | // Verify dependency relationships are preserved 470 | const task1 = rawData['in-progress']?.tasks?.find((t) => t.id === 1); 471 | const task2 = rawData['in-progress']?.tasks?.find((t) => t.id === 2); 472 | const task3 = rawData['in-progress']?.tasks?.find((t) => t.id === 3); 473 | const task4 = rawData['in-progress']?.tasks?.find((t) => t.id === 4); 474 | 475 | expect(task1?.dependencies).toEqual([2]); 476 | expect(task2?.dependencies).toEqual([3]); 477 | expect(task3?.dependencies).toEqual([4]); 478 | expect(task4?.dependencies).toEqual([]); 479 | }); 480 | 481 | it('should handle cross-tag dependency resolution using actual move functions', async () => { 482 | // Create data with cross-tag dependencies 483 | const crossTagData = { 484 | backlog: { 485 | tasks: [ 486 | { id: 1, title: 'Task 1', dependencies: [5], status: 'pending' }, // Depends on task in in-progress 487 | { id: 2, title: 'Task 2', dependencies: [], status: 'pending' } 488 | ] 489 | }, 490 | 'in-progress': { 491 | tasks: [ 492 | { id: 5, title: 'Task 5', dependencies: [], status: 'in-progress' } 493 | ] 494 | } 495 | }; 496 | 497 | // Set up mock file system with cross-tag dependency data 498 | mockFs({ 499 | [testDataDir]: { 500 | 'tasks.json': JSON.stringify(crossTagData, null, 2) 501 | } 502 | }); 503 | 504 | // Test moving Task 1 which depends on Task 5 in another tag 505 | const result = await moveTasksBetweenTags( 506 | testTasksPath, 507 | ['1'], // Task 1 508 | 'backlog', 509 | 'in-progress', 510 | { withDependencies: false, ignoreDependencies: false }, 511 | { projectRoot: testDataDir } 512 | ); 513 | 514 | // Verify the move operation was successful 515 | expect(result).toBeDefined(); 516 | expect(result.message).toContain( 517 | 'Successfully moved 1 tasks from "backlog" to "in-progress"' 518 | ); 519 | 520 | // Read the updated data to verify the move actually happened 521 | const updatedData = readJSON(testTasksPath, null, 'backlog'); 522 | const rawData = updatedData._rawTaggedData || updatedData; 523 | 524 | // Verify Task 1 is no longer in backlog 525 | const taskInBacklog = rawData.backlog?.tasks?.find((t) => t.id === 1); 526 | expect(taskInBacklog).toBeUndefined(); 527 | 528 | // Verify Task 1 is now in in-progress with its dependency preserved 529 | const taskInProgress = rawData['in-progress']?.tasks?.find( 530 | (t) => t.id === 1 531 | ); 532 | expect(taskInProgress).toBeDefined(); 533 | expect(taskInProgress.title).toBe('Task 1'); 534 | expect(taskInProgress.dependencies).toEqual([5]); // Cross-tag dependency preserved 535 | }); 536 | }); 537 | }); 538 | ``` -------------------------------------------------------------------------------- /apps/extension/CHANGELOG.md: -------------------------------------------------------------------------------- ```markdown 1 | # Change Log 2 | 3 | ## 0.25.4 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [[`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d)]: 8 | - [email protected] 9 | 10 | ## 0.25.3 11 | 12 | ### Patch Changes 13 | 14 | - Updated dependencies [[`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011)]: 15 | - [email protected] 16 | 17 | ## 0.25.2 18 | 19 | ### Patch Changes 20 | 21 | - Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e), [`c911608`](https://github.com/eyaltoledano/claude-task-master/commit/c911608f60454253f4e024b57ca84e5a5a53f65c), [`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]: 22 | - [email protected] 23 | 24 | ## 0.25.2-rc.1 25 | 26 | ### Patch Changes 27 | 28 | - Updated dependencies [[`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]: 29 | - [email protected] 30 | 31 | ## 0.25.2-rc.0 32 | 33 | ### Patch Changes 34 | 35 | - Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e)]: 36 | - [email protected] 37 | 38 | ## 0.25.0 39 | 40 | ### Minor Changes 41 | 42 | - [#1200](https://github.com/eyaltoledano/claude-task-master/pull/1200) [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142) Thanks [@eyaltoledano](https://github.com/eyaltoledano)! - Add "Start Task" button to VS Code extension for seamless Claude Code integration 43 | 44 | You can now click a "Start Task" button directly in the Task Master extension which will open a new terminal and automatically execute the task using Claude Code. This provides a seamless workflow from viewing tasks in the extension to implementing them without leaving VS Code. 45 | 46 | - [#1201](https://github.com/eyaltoledano/claude-task-master/pull/1201) [`83af314`](https://github.com/eyaltoledano/claude-task-master/commit/83af314879fc0e563581161c60d2bd089899313e) Thanks [@losolosol](https://github.com/losolosol)! - Added a Start Build button to the VSCODE Task Properties Right Panel 47 | 48 | ### Patch Changes 49 | 50 | - [#1229](https://github.com/eyaltoledano/claude-task-master/pull/1229) [`674d1f6`](https://github.com/eyaltoledano/claude-task-master/commit/674d1f6de7ea98116b61bdae6198bafe6c4e7c1a) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix MCP not connecting to new Taskmaster version 51 | 52 | - Updated dependencies [[`4e12643`](https://github.com/eyaltoledano/claude-task-master/commit/4e126430a092fb54afb035514fb3d46115714f97), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142), [`a621ff0`](https://github.com/eyaltoledano/claude-task-master/commit/a621ff05eafb51a147a9aabd7b37ddc0e45b0869), [`e6de285`](https://github.com/eyaltoledano/claude-task-master/commit/e6de285ceacb0a397e952a63435cd32a9c731515), [`fce8414`](https://github.com/eyaltoledano/claude-task-master/commit/fce841490a9ebbf1801a42dd8a29397379cf1142)]: 53 | - [email protected] 54 | 55 | ## 0.25.0-rc.0 56 | 57 | ### Minor Changes 58 | 59 | - [#1201](https://github.com/eyaltoledano/claude-task-master/pull/1201) [`83af314`](https://github.com/eyaltoledano/claude-task-master/commit/83af314879fc0e563581161c60d2bd089899313e) Thanks [@losolosol](https://github.com/losolosol)! - Added a Start Build button to the VSCODE Task Properties Right Panel 60 | 61 | ### Patch Changes 62 | 63 | - Updated dependencies [[`137ef36`](https://github.com/eyaltoledano/claude-task-master/commit/137ef362789a9cdfdb1925e35e0438c1fa6c69ee)]: 64 | - [email protected] 65 | 66 | ## 0.24.2 67 | 68 | ### Patch Changes 69 | 70 | - Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`df26c65`](https://github.com/eyaltoledano/claude-task-master/commit/df26c65632000874a73504963b08f18c46283144), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]: 71 | - [email protected] 72 | 73 | ## 0.24.2-rc.1 74 | 75 | ### Patch Changes 76 | 77 | - Updated dependencies [[`c4f92f6`](https://github.com/eyaltoledano/claude-task-master/commit/c4f92f6a0aee3435c56eb8d27d9aa9204284833e)]: 78 | - [email protected] 79 | 80 | ## 0.24.2-rc.0 81 | 82 | ### Patch Changes 83 | 84 | - Updated dependencies [[`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`37af0f1`](https://github.com/eyaltoledano/claude-task-master/commit/37af0f191227a68d119b7f89a377bf932ee3ac66), [`8783708`](https://github.com/eyaltoledano/claude-task-master/commit/8783708e5e3389890a78fcf685d3da0580e73b3f), [`4dad2fd`](https://github.com/eyaltoledano/claude-task-master/commit/4dad2fd613ceac56a65ae9d3c1c03092b8860ac9)]: 85 | - [email protected] 86 | 87 | ## 0.24.1 88 | 89 | ### Patch Changes 90 | 91 | - Updated dependencies [[`8933557`](https://github.com/eyaltoledano/claude-task-master/commit/89335578ffffc65504b2055c0c85aa7521e5e79b), [`db720a9`](https://github.com/eyaltoledano/claude-task-master/commit/db720a954d390bb44838cd021b8813dde8f3d8de)]: 92 | - [email protected] 93 | 94 | ## 0.24.0 95 | 96 | ### Minor Changes 97 | 98 | - [#1100](https://github.com/eyaltoledano/claude-task-master/pull/1100) [`30ca144`](https://github.com/eyaltoledano/claude-task-master/commit/30ca144231c36a6c63911f20adc225d38fb15a2f) Thanks [@vedovelli](https://github.com/vedovelli)! - Display current task ID on task details page 99 | 100 | ### Patch Changes 101 | 102 | - Updated dependencies [[`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1), [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573), [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e), [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d), [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397), [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5)]: 103 | - [email protected] 104 | 105 | ## 0.24.0-rc.0 106 | 107 | ### Minor Changes 108 | 109 | - [#1040](https://github.com/eyaltoledano/claude-task-master/pull/1040) [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1) Thanks [@DomVidja](https://github.com/DomVidja)! - "Add Kilo Code profile integration with custom modes and MCP configuration" 110 | 111 | - [#1100](https://github.com/eyaltoledano/claude-task-master/pull/1100) [`30ca144`](https://github.com/eyaltoledano/claude-task-master/commit/30ca144231c36a6c63911f20adc225d38fb15a2f) Thanks [@vedovelli](https://github.com/vedovelli)! - Display current task ID on task details page 112 | 113 | ### Patch Changes 114 | 115 | - Updated dependencies [[`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`fc47714`](https://github.com/eyaltoledano/claude-task-master/commit/fc477143400fd11d953727bf1b4277af5ad308d1), [`782728f`](https://github.com/eyaltoledano/claude-task-master/commit/782728ff95aa2e3b766d48273b57f6c6753e8573), [`3dee60d`](https://github.com/eyaltoledano/claude-task-master/commit/3dee60dc3d566e3cff650accb30f994b8bb3a15e), [`e3ed4d7`](https://github.com/eyaltoledano/claude-task-master/commit/e3ed4d7c14b56894d7da675eb2b757423bea8f9d), [`04e11b5`](https://github.com/eyaltoledano/claude-task-master/commit/04e11b5e828597c0ba5b82ca7d5fb6f933e4f1e8), [`95640dc`](https://github.com/eyaltoledano/claude-task-master/commit/95640dcde87ce7879858c0a951399fb49f3b6397), [`311b243`](https://github.com/eyaltoledano/claude-task-master/commit/311b2433e23c771c8d3a4d3f5ac577302b8321e5)]: 116 | - [email protected] 117 | 118 | ## 0.23.1 119 | 120 | ### Patch Changes 121 | 122 | - [#1090](https://github.com/eyaltoledano/claude-task-master/pull/1090) [`a464e55`](https://github.com/eyaltoledano/claude-task-master/commit/a464e550b886ef81b09df80588fe5881bce83d93) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix issues with some users not being able to connect to Taskmaster MCP server while using the extension 123 | 124 | - Updated dependencies [[`4357af3`](https://github.com/eyaltoledano/claude-task-master/commit/4357af3f13859d90bca8795215e5d5f1d94abde5), [`e495b2b`](https://github.com/eyaltoledano/claude-task-master/commit/e495b2b55950ee54c7d0f1817d8530e28bd79c05), [`36468f3`](https://github.com/eyaltoledano/claude-task-master/commit/36468f3c93faf4035a5c442ccbc501077f3440f1), [`e495b2b`](https://github.com/eyaltoledano/claude-task-master/commit/e495b2b55950ee54c7d0f1817d8530e28bd79c05), [`e495b2b`](https://github.com/eyaltoledano/claude-task-master/commit/e495b2b55950ee54c7d0f1817d8530e28bd79c05), [`75c514c`](https://github.com/eyaltoledano/claude-task-master/commit/75c514cf5b2ca47f95c0ad7fa92654a4f2a6be4b), [`4bb6370`](https://github.com/eyaltoledano/claude-task-master/commit/4bb63706b80c28d1b2d782ba868a725326f916c7)]: 125 | - [email protected] 126 | 127 | ## 0.23.1-rc.1 128 | 129 | ### Patch Changes 130 | 131 | - Updated dependencies [[`75c514c`](https://github.com/eyaltoledano/claude-task-master/commit/75c514cf5b2ca47f95c0ad7fa92654a4f2a6be4b)]: 132 | - [email protected] 133 | 134 | ## 0.23.1-rc.0 135 | 136 | ### Patch Changes 137 | 138 | - [#1090](https://github.com/eyaltoledano/claude-task-master/pull/1090) [`a464e55`](https://github.com/eyaltoledano/claude-task-master/commit/a464e550b886ef81b09df80588fe5881bce83d93) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix issues with some users not being able to connect to Taskmaster MCP server while using the extension 139 | 140 | - Updated dependencies [[`4357af3`](https://github.com/eyaltoledano/claude-task-master/commit/4357af3f13859d90bca8795215e5d5f1d94abde5), [`36468f3`](https://github.com/eyaltoledano/claude-task-master/commit/36468f3c93faf4035a5c442ccbc501077f3440f1), [`4bb6370`](https://github.com/eyaltoledano/claude-task-master/commit/4bb63706b80c28d1b2d782ba868a725326f916c7)]: 141 | - [email protected] 142 | 143 | ## 0.23.0 144 | 145 | ### Minor Changes 146 | 147 | - [#1064](https://github.com/eyaltoledano/claude-task-master/pull/1064) [`b82d858`](https://github.com/eyaltoledano/claude-task-master/commit/b82d858f81a1e702ad59d84d5ae8a2ca84359a83) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - 🎉 **Introducing TaskMaster Extension!** 148 | 149 | We're thrilled to launch the first version of our Code extension, bringing the power of TaskMaster directly into your favorite code editor. While this is our initial release and we've kept things focused, it already packs powerful features to supercharge your development workflow. 150 | 151 | ## ✨ Key Features 152 | 153 | ### 📋 Visual Task Management 154 | - **Kanban Board View**: Visualize all your tasks in an intuitive board layout directly in VS Code 155 | - **Drag & Drop**: Easily change task status by dragging cards between columns 156 | - **Real-time Updates**: See changes instantly as you work through your project 157 | 158 | ### 🏷️ Multi-Context Support 159 | - **Tag Switching**: Seamlessly switch between different project contexts/tags 160 | - **Isolated Workflows**: Keep different features or experiments organized separately 161 | 162 | ### 🤖 AI-Powered Task Updates 163 | - **Smart Updates**: Use TaskMaster's AI capabilities to update tasks and subtasks 164 | - **Context-Aware**: Leverages your existing TaskMaster configuration and models 165 | 166 | ### 📊 Rich Task Information 167 | - **Complexity Scores**: See task complexity ratings at a glance 168 | - **Subtask Visualization**: Expand tasks to view and manage subtasks 169 | - **Dependency Graphs**: Understand task relationships and dependencies visually 170 | 171 | ### ⚙️ Configuration Management 172 | - **Visual Config Editor**: View and understand your `.taskmaster/config.json` settings 173 | - **Easy Access**: No more manual JSON editing for common configuration tasks 174 | 175 | ### 🚀 Quick Actions 176 | - **Status Updates**: Change task status with a single click 177 | - **Task Details**: Access full task information without leaving VS Code 178 | - **Integrated Commands**: All TaskMaster commands available through the command palette 179 | 180 | ## 🎯 What's Next? 181 | 182 | This is just the beginning! We wanted to get a solid foundation into your hands quickly. The extension will evolve rapidly with your feedback, adding more advanced features, better visualizations, and deeper integration with your development workflow. 183 | 184 | Thank you for being part of the TaskMaster journey. Your workflow has never looked better! 🚀 185 | 186 | ## 0.23.0-rc.1 187 | 188 | ### Minor Changes 189 | 190 | - [#1064](https://github.com/eyaltoledano/claude-task-master/pull/1064) [`b82d858`](https://github.com/eyaltoledano/claude-task-master/commit/b82d858f81a1e702ad59d84d5ae8a2ca84359a83) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - 🎉 **Introducing TaskMaster Extension!** 191 | 192 | We're thrilled to launch the first version of our Code extension, bringing the power of TaskMaster directly into your favorite code editor. While this is our initial release and we've kept things focused, it already packs powerful features to supercharge your development workflow. 193 | 194 | ## ✨ Key Features 195 | 196 | ### 📋 Visual Task Management 197 | - **Kanban Board View**: Visualize all your tasks in an intuitive board layout directly in VS Code 198 | - **Drag & Drop**: Easily change task status by dragging cards between columns 199 | - **Real-time Updates**: See changes instantly as you work through your project 200 | 201 | ### 🏷️ Multi-Context Support 202 | - **Tag Switching**: Seamlessly switch between different project contexts/tags 203 | - **Isolated Workflows**: Keep different features or experiments organized separately 204 | 205 | ### 🤖 AI-Powered Task Updates 206 | - **Smart Updates**: Use TaskMaster's AI capabilities to update tasks and subtasks 207 | - **Context-Aware**: Leverages your existing TaskMaster configuration and models 208 | 209 | ### 📊 Rich Task Information 210 | - **Complexity Scores**: See task complexity ratings at a glance 211 | - **Subtask Visualization**: Expand tasks to view and manage subtasks 212 | - **Dependency Graphs**: Understand task relationships and dependencies visually 213 | 214 | ### ⚙️ Configuration Management 215 | - **Visual Config Editor**: View and understand your `.taskmaster/config.json` settings 216 | - **Easy Access**: No more manual JSON editing for common configuration tasks 217 | 218 | ### 🚀 Quick Actions 219 | - **Status Updates**: Change task status with a single click 220 | - **Task Details**: Access full task information without leaving VS Code 221 | - **Integrated Commands**: All TaskMaster commands available through the command palette 222 | 223 | ## 🎯 What's Next? 224 | 225 | This is just the beginning! We wanted to get a solid foundation into your hands quickly. The extension will evolve rapidly with your feedback, adding more advanced features, better visualizations, and deeper integration with your development workflow. 226 | 227 | Thank you for being part of the TaskMaster journey. Your workflow has never looked better! 🚀 228 | 229 | ## 0.23.0-rc.0 230 | 231 | ### Minor Changes 232 | 233 | - [#997](https://github.com/eyaltoledano/claude-task-master/pull/997) [`64302dc`](https://github.com/eyaltoledano/claude-task-master/commit/64302dc1918f673fcdac05b29411bf76ffe93505) Thanks [@DavidMaliglowka](https://github.com/DavidMaliglowka)! - 🎉 **Introducing TaskMaster Extension!** 234 | 235 | We're thrilled to launch the first version of our Code extension, bringing the power of TaskMaster directly into your favorite code editor. While this is our initial release and we've kept things focused, it already packs powerful features to supercharge your development workflow. 236 | 237 | ## ✨ Key Features 238 | 239 | ### 📋 Visual Task Management 240 | - **Kanban Board View**: Visualize all your tasks in an intuitive board layout directly in VS Code 241 | - **Drag & Drop**: Easily change task status by dragging cards between columns 242 | - **Real-time Updates**: See changes instantly as you work through your project 243 | 244 | ### 🏷️ Multi-Context Support 245 | - **Tag Switching**: Seamlessly switch between different project contexts/tags 246 | - **Isolated Workflows**: Keep different features or experiments organized separately 247 | 248 | ### 🤖 AI-Powered Task Updates 249 | - **Smart Updates**: Use TaskMaster's AI capabilities to update tasks and subtasks 250 | - **Context-Aware**: Leverages your existing TaskMaster configuration and models 251 | 252 | ### 📊 Rich Task Information 253 | - **Complexity Scores**: See task complexity ratings at a glance 254 | - **Subtask Visualization**: Expand tasks to view and manage subtasks 255 | - **Dependency Graphs**: Understand task relationships and dependencies visually 256 | 257 | ### ⚙️ Configuration Management 258 | - **Visual Config Editor**: View and understand your `.taskmaster/config.json` settings 259 | - **Easy Access**: No more manual JSON editing for common configuration tasks 260 | 261 | ### 🚀 Quick Actions 262 | - **Status Updates**: Change task status with a single click 263 | - **Task Details**: Access full task information without leaving VS Code 264 | - **Integrated Commands**: All TaskMaster commands available through the command palette 265 | 266 | ## 🎯 What's Next? 267 | 268 | This is just the beginning! We wanted to get a solid foundation into your hands quickly. The extension will evolve rapidly with your feedback, adding more advanced features, better visualizations, and deeper integration with your development workflow. 269 | 270 | Thank you for being part of the TaskMaster journey. Your workflow has never looked better! 🚀 271 | ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/update-tasks.js: -------------------------------------------------------------------------------- ```javascript 1 | import path from 'path'; 2 | import chalk from 'chalk'; 3 | import boxen from 'boxen'; 4 | import Table from 'cli-table3'; 5 | import { z } from 'zod'; // Keep Zod for post-parsing validation 6 | 7 | import { 8 | log as consoleLog, 9 | readJSON, 10 | writeJSON, 11 | truncate, 12 | isSilentMode 13 | } from '../utils.js'; 14 | 15 | import { 16 | getStatusWithColor, 17 | startLoadingIndicator, 18 | stopLoadingIndicator, 19 | displayAiUsageSummary 20 | } from '../ui.js'; 21 | 22 | import { getDebugFlag, hasCodebaseAnalysis } from '../config-manager.js'; 23 | import { getPromptManager } from '../prompt-manager.js'; 24 | import generateTaskFiles from './generate-task-files.js'; 25 | import { generateTextService } from '../ai-services-unified.js'; 26 | import { getModelConfiguration } from './models.js'; 27 | import { ContextGatherer } from '../utils/contextGatherer.js'; 28 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js'; 29 | import { flattenTasksWithSubtasks, findProjectRoot } from '../utils.js'; 30 | 31 | // Zod schema for validating the structure of tasks AFTER parsing 32 | const updatedTaskSchema = z 33 | .object({ 34 | id: z.number().int(), 35 | title: z.string(), 36 | description: z.string(), 37 | status: z.string(), 38 | dependencies: z.array(z.union([z.number().int(), z.string()])), 39 | priority: z.string().nullable(), 40 | details: z.string().nullable(), 41 | testStrategy: z.string().nullable(), 42 | subtasks: z.array(z.any()).nullable() // Keep subtasks flexible for now 43 | }) 44 | .strip(); // Allow potential extra fields during parsing if needed, then validate structure 45 | 46 | // Preprocessing schema that adds defaults before validation 47 | const preprocessTaskSchema = z.preprocess((task) => { 48 | // Ensure task is an object 49 | if (typeof task !== 'object' || task === null) { 50 | return {}; 51 | } 52 | 53 | // Return task with defaults for missing fields 54 | return { 55 | ...task, 56 | // Add defaults for required fields if missing 57 | id: task.id ?? 0, 58 | title: task.title ?? 'Untitled Task', 59 | description: task.description ?? '', 60 | status: task.status ?? 'pending', 61 | dependencies: Array.isArray(task.dependencies) ? task.dependencies : [], 62 | // Optional fields - preserve undefined/null distinction 63 | priority: task.hasOwnProperty('priority') ? task.priority : null, 64 | details: task.hasOwnProperty('details') ? task.details : null, 65 | testStrategy: task.hasOwnProperty('testStrategy') 66 | ? task.testStrategy 67 | : null, 68 | subtasks: Array.isArray(task.subtasks) 69 | ? task.subtasks 70 | : task.subtasks === null 71 | ? null 72 | : [] 73 | }; 74 | }, updatedTaskSchema); 75 | 76 | const updatedTaskArraySchema = z.array(updatedTaskSchema); 77 | const preprocessedTaskArraySchema = z.array(preprocessTaskSchema); 78 | 79 | /** 80 | * Parses an array of task objects from AI's text response. 81 | * @param {string} text - Response text from AI. 82 | * @param {number} expectedCount - Expected number of tasks. 83 | * @param {Function | Object} logFn - The logging function or MCP log object. 84 | * @param {boolean} isMCP - Flag indicating if logFn is MCP logger. 85 | * @returns {Array} Parsed and validated tasks array. 86 | * @throws {Error} If parsing or validation fails. 87 | */ 88 | function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) { 89 | const report = (level, ...args) => { 90 | if (isMCP) { 91 | if (typeof logFn[level] === 'function') logFn[level](...args); 92 | else logFn.info(...args); 93 | } else if (!isSilentMode()) { 94 | // Check silent mode for consoleLog 95 | consoleLog(level, ...args); 96 | } 97 | }; 98 | 99 | report( 100 | 'info', 101 | 'Attempting to parse updated tasks array from text response...' 102 | ); 103 | if (!text || text.trim() === '') 104 | throw new Error('AI response text is empty.'); 105 | 106 | let cleanedResponse = text.trim(); 107 | const originalResponseForDebug = cleanedResponse; 108 | let parseMethodUsed = 'raw'; // Track which method worked 109 | 110 | // --- NEW Step 1: Try extracting between [] first --- 111 | const firstBracketIndex = cleanedResponse.indexOf('['); 112 | const lastBracketIndex = cleanedResponse.lastIndexOf(']'); 113 | let potentialJsonFromArray = null; 114 | 115 | if (firstBracketIndex !== -1 && lastBracketIndex > firstBracketIndex) { 116 | potentialJsonFromArray = cleanedResponse.substring( 117 | firstBracketIndex, 118 | lastBracketIndex + 1 119 | ); 120 | // Basic check to ensure it's not just "[]" or malformed 121 | if (potentialJsonFromArray.length <= 2) { 122 | potentialJsonFromArray = null; // Ignore empty array 123 | } 124 | } 125 | 126 | // If [] extraction yielded something, try parsing it immediately 127 | if (potentialJsonFromArray) { 128 | try { 129 | const testParse = JSON.parse(potentialJsonFromArray); 130 | // It worked! Use this as the primary cleaned response. 131 | cleanedResponse = potentialJsonFromArray; 132 | parseMethodUsed = 'brackets'; 133 | } catch (e) { 134 | report( 135 | 'info', 136 | 'Content between [] looked promising but failed initial parse. Proceeding to other methods.' 137 | ); 138 | // Reset cleanedResponse to original if bracket parsing failed 139 | cleanedResponse = originalResponseForDebug; 140 | } 141 | } 142 | 143 | // --- Step 2: If bracket parsing didn't work or wasn't applicable, try code block extraction --- 144 | if (parseMethodUsed === 'raw') { 145 | // Only look for ```json blocks now 146 | const codeBlockMatch = cleanedResponse.match( 147 | /```json\s*([\s\S]*?)\s*```/i // Only match ```json 148 | ); 149 | if (codeBlockMatch) { 150 | cleanedResponse = codeBlockMatch[1].trim(); 151 | parseMethodUsed = 'codeblock'; 152 | report('info', 'Extracted JSON content from JSON Markdown code block.'); 153 | } else { 154 | report('info', 'No JSON code block found.'); 155 | // --- Step 3: If code block failed, try stripping prefixes --- 156 | const commonPrefixes = [ 157 | 'json\n', 158 | 'javascript\n', // Keep checking common prefixes just in case 159 | 'python\n', 160 | 'here are the updated tasks:', 161 | 'here is the updated json:', 162 | 'updated tasks:', 163 | 'updated json:', 164 | 'response:', 165 | 'output:' 166 | ]; 167 | let prefixFound = false; 168 | for (const prefix of commonPrefixes) { 169 | if (cleanedResponse.toLowerCase().startsWith(prefix)) { 170 | cleanedResponse = cleanedResponse.substring(prefix.length).trim(); 171 | parseMethodUsed = 'prefix'; 172 | report('info', `Stripped prefix: "${prefix.trim()}"`); 173 | prefixFound = true; 174 | break; 175 | } 176 | } 177 | if (!prefixFound) { 178 | report( 179 | 'warn', 180 | 'Response does not appear to contain [], JSON code block, or known prefix. Attempting raw parse.' 181 | ); 182 | } 183 | } 184 | } 185 | 186 | // --- Step 4: Attempt final parse --- 187 | let parsedTasks; 188 | try { 189 | parsedTasks = JSON.parse(cleanedResponse); 190 | } catch (parseError) { 191 | report('error', `Failed to parse JSON array: ${parseError.message}`); 192 | report( 193 | 'error', 194 | `Extraction method used: ${parseMethodUsed}` // Log which method failed 195 | ); 196 | report( 197 | 'error', 198 | `Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}` 199 | ); 200 | report( 201 | 'error', 202 | `Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}` 203 | ); 204 | throw new Error( 205 | `Failed to parse JSON response array: ${parseError.message}` 206 | ); 207 | } 208 | 209 | // --- Step 5 & 6: Validate Array structure and Zod schema --- 210 | if (!Array.isArray(parsedTasks)) { 211 | report( 212 | 'error', 213 | `Parsed content is not an array. Type: ${typeof parsedTasks}` 214 | ); 215 | report( 216 | 'error', 217 | `Parsed content sample: ${JSON.stringify(parsedTasks).substring(0, 200)}` 218 | ); 219 | throw new Error('Parsed AI response is not a valid JSON array.'); 220 | } 221 | 222 | report('info', `Successfully parsed ${parsedTasks.length} potential tasks.`); 223 | if (expectedCount && parsedTasks.length !== expectedCount) { 224 | report( 225 | 'warn', 226 | `Expected ${expectedCount} tasks, but parsed ${parsedTasks.length}.` 227 | ); 228 | } 229 | 230 | // Log missing fields for debugging before preprocessing 231 | let hasWarnings = false; 232 | parsedTasks.forEach((task, index) => { 233 | const missingFields = []; 234 | if (!task.hasOwnProperty('id')) missingFields.push('id'); 235 | if (!task.hasOwnProperty('status')) missingFields.push('status'); 236 | if (!task.hasOwnProperty('dependencies')) 237 | missingFields.push('dependencies'); 238 | 239 | if (missingFields.length > 0) { 240 | hasWarnings = true; 241 | report( 242 | 'warn', 243 | `Task ${index} is missing fields: ${missingFields.join(', ')} - will use defaults` 244 | ); 245 | } 246 | }); 247 | 248 | if (hasWarnings) { 249 | report( 250 | 'warn', 251 | 'Some tasks were missing required fields. Applying defaults...' 252 | ); 253 | } 254 | 255 | // Use the preprocessing schema to add defaults and validate 256 | const preprocessResult = preprocessedTaskArraySchema.safeParse(parsedTasks); 257 | 258 | if (!preprocessResult.success) { 259 | // This should rarely happen now since preprocessing adds defaults 260 | report('error', 'Failed to validate task array even after preprocessing.'); 261 | preprocessResult.error.errors.forEach((err) => { 262 | report('error', ` - Path '${err.path.join('.')}': ${err.message}`); 263 | }); 264 | 265 | throw new Error( 266 | `AI response failed validation: ${preprocessResult.error.message}` 267 | ); 268 | } 269 | 270 | report('info', 'Successfully validated and transformed task structure.'); 271 | return preprocessResult.data.slice( 272 | 0, 273 | expectedCount || preprocessResult.data.length 274 | ); 275 | } 276 | 277 | /** 278 | * Update tasks based on new context using the unified AI service. 279 | * @param {string} tasksPath - Path to the tasks.json file 280 | * @param {number} fromId - Task ID to start updating from 281 | * @param {string} prompt - Prompt with new context 282 | * @param {boolean} [useResearch=false] - Whether to use the research AI role. 283 | * @param {Object} context - Context object containing session and mcpLog. 284 | * @param {Object} [context.session] - Session object from MCP server. 285 | * @param {Object} [context.mcpLog] - MCP logger object. 286 | * @param {string} [context.tag] - Tag for the task 287 | * @param {string} [outputFormat='text'] - Output format ('text' or 'json'). 288 | */ 289 | async function updateTasks( 290 | tasksPath, 291 | fromId, 292 | prompt, 293 | useResearch = false, 294 | context = {}, 295 | outputFormat = 'text' // Default to text for CLI 296 | ) { 297 | const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context; 298 | // Use mcpLog if available, otherwise use the imported consoleLog function 299 | const logFn = mcpLog || consoleLog; 300 | // Flag to easily check which logger type we have 301 | const isMCP = !!mcpLog; 302 | 303 | if (isMCP) 304 | logFn.info(`updateTasks called with context: session=${!!session}`); 305 | else logFn('info', `updateTasks called`); // CLI log 306 | 307 | try { 308 | if (isMCP) logFn.info(`Updating tasks from ID ${fromId}`); 309 | else 310 | logFn( 311 | 'info', 312 | `Updating tasks from ID ${fromId} with prompt: "${prompt}"` 313 | ); 314 | 315 | // Determine project root 316 | const projectRoot = providedProjectRoot || findProjectRoot(); 317 | if (!projectRoot) { 318 | throw new Error('Could not determine project root directory'); 319 | } 320 | 321 | // --- Task Loading/Filtering (Updated to pass projectRoot and tag) --- 322 | const data = readJSON(tasksPath, projectRoot, tag); 323 | if (!data || !data.tasks) 324 | throw new Error(`No valid tasks found in ${tasksPath}`); 325 | const tasksToUpdate = data.tasks.filter( 326 | (task) => task.id >= fromId && task.status !== 'done' 327 | ); 328 | if (tasksToUpdate.length === 0) { 329 | if (isMCP) 330 | logFn.info(`No tasks to update (ID >= ${fromId} and not 'done').`); 331 | else 332 | logFn('info', `No tasks to update (ID >= ${fromId} and not 'done').`); 333 | if (outputFormat === 'text') console.log(/* yellow message */); 334 | return; // Nothing to do 335 | } 336 | // --- End Task Loading/Filtering --- 337 | 338 | // --- Context Gathering --- 339 | let gatheredContext = ''; 340 | try { 341 | const contextGatherer = new ContextGatherer(projectRoot, tag); 342 | const allTasksFlat = flattenTasksWithSubtasks(data.tasks); 343 | const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update'); 344 | const searchResults = fuzzySearch.findRelevantTasks(prompt, { 345 | maxResults: 5, 346 | includeSelf: true 347 | }); 348 | const relevantTaskIds = fuzzySearch.getTaskIds(searchResults); 349 | 350 | const tasksToUpdateIds = tasksToUpdate.map((t) => t.id.toString()); 351 | const finalTaskIds = [ 352 | ...new Set([...tasksToUpdateIds, ...relevantTaskIds]) 353 | ]; 354 | 355 | if (finalTaskIds.length > 0) { 356 | const contextResult = await contextGatherer.gather({ 357 | tasks: finalTaskIds, 358 | format: 'research' 359 | }); 360 | gatheredContext = contextResult.context || ''; 361 | } 362 | } catch (contextError) { 363 | logFn( 364 | 'warn', 365 | `Could not gather additional context: ${contextError.message}` 366 | ); 367 | } 368 | // --- End Context Gathering --- 369 | 370 | // --- Display Tasks to Update (CLI Only - Unchanged) --- 371 | if (outputFormat === 'text') { 372 | // Show the tasks that will be updated 373 | const table = new Table({ 374 | head: [ 375 | chalk.cyan.bold('ID'), 376 | chalk.cyan.bold('Title'), 377 | chalk.cyan.bold('Status') 378 | ], 379 | colWidths: [5, 70, 20] 380 | }); 381 | 382 | tasksToUpdate.forEach((task) => { 383 | table.push([ 384 | task.id, 385 | truncate(task.title, 57), 386 | getStatusWithColor(task.status) 387 | ]); 388 | }); 389 | 390 | console.log( 391 | boxen(chalk.white.bold(`Updating ${tasksToUpdate.length} tasks`), { 392 | padding: 1, 393 | borderColor: 'blue', 394 | borderStyle: 'round', 395 | margin: { top: 1, bottom: 0 } 396 | }) 397 | ); 398 | 399 | console.log(table.toString()); 400 | 401 | // Display a message about how completed subtasks are handled 402 | console.log( 403 | boxen( 404 | chalk.cyan.bold('How Completed Subtasks Are Handled:') + 405 | '\n\n' + 406 | chalk.white( 407 | '• Subtasks marked as "done" or "completed" will be preserved\n' 408 | ) + 409 | chalk.white( 410 | '• New subtasks will build upon what has already been completed\n' 411 | ) + 412 | chalk.white( 413 | '• If completed work needs revision, a new subtask will be created instead of modifying done items\n' 414 | ) + 415 | chalk.white( 416 | '• This approach maintains a clear record of completed work and new requirements' 417 | ), 418 | { 419 | padding: 1, 420 | borderColor: 'blue', 421 | borderStyle: 'round', 422 | margin: { top: 1, bottom: 1 } 423 | } 424 | ) 425 | ); 426 | } 427 | // --- End Display Tasks --- 428 | 429 | // --- Build Prompts (Using PromptManager) --- 430 | // Load prompts using PromptManager 431 | const promptManager = getPromptManager(); 432 | const { systemPrompt, userPrompt } = await promptManager.loadPrompt( 433 | 'update-tasks', 434 | { 435 | tasks: tasksToUpdate, 436 | updatePrompt: prompt, 437 | useResearch, 438 | projectContext: gatheredContext, 439 | hasCodebaseAnalysis: hasCodebaseAnalysis( 440 | useResearch, 441 | projectRoot, 442 | session 443 | ), 444 | projectRoot: projectRoot 445 | } 446 | ); 447 | // --- End Build Prompts --- 448 | 449 | // --- AI Call --- 450 | let loadingIndicator = null; 451 | let aiServiceResponse = null; 452 | 453 | if (!isMCP && outputFormat === 'text') { 454 | loadingIndicator = startLoadingIndicator('Updating tasks with AI...\n'); 455 | } 456 | 457 | try { 458 | // Determine role based on research flag 459 | const serviceRole = useResearch ? 'research' : 'main'; 460 | 461 | // Call the unified AI service 462 | aiServiceResponse = await generateTextService({ 463 | role: serviceRole, 464 | session: session, 465 | projectRoot: projectRoot, 466 | systemPrompt: systemPrompt, 467 | prompt: userPrompt, 468 | commandName: 'update-tasks', 469 | outputType: isMCP ? 'mcp' : 'cli' 470 | }); 471 | 472 | if (loadingIndicator) 473 | stopLoadingIndicator(loadingIndicator, 'AI update complete.'); 474 | 475 | // Use the mainResult (text) for parsing 476 | const parsedUpdatedTasks = parseUpdatedTasksFromText( 477 | aiServiceResponse.mainResult, 478 | tasksToUpdate.length, 479 | logFn, 480 | isMCP 481 | ); 482 | 483 | // --- Update Tasks Data (Updated writeJSON call) --- 484 | if (!Array.isArray(parsedUpdatedTasks)) { 485 | // Should be caught by parser, but extra check 486 | throw new Error( 487 | 'Parsed AI response for updated tasks was not an array.' 488 | ); 489 | } 490 | if (isMCP) 491 | logFn.info( 492 | `Received ${parsedUpdatedTasks.length} updated tasks from AI.` 493 | ); 494 | else 495 | logFn( 496 | 'info', 497 | `Received ${parsedUpdatedTasks.length} updated tasks from AI.` 498 | ); 499 | // Create a map for efficient lookup 500 | const updatedTasksMap = new Map( 501 | parsedUpdatedTasks.map((task) => [task.id, task]) 502 | ); 503 | 504 | let actualUpdateCount = 0; 505 | data.tasks.forEach((task, index) => { 506 | if (updatedTasksMap.has(task.id)) { 507 | // Only update if the task was part of the set sent to AI 508 | const updatedTask = updatedTasksMap.get(task.id); 509 | // Merge the updated task with the existing one to preserve fields like subtasks 510 | data.tasks[index] = { 511 | ...task, // Keep all existing fields 512 | ...updatedTask, // Override with updated fields 513 | // Ensure subtasks field is preserved if not provided by AI 514 | subtasks: 515 | updatedTask.subtasks !== undefined 516 | ? updatedTask.subtasks 517 | : task.subtasks 518 | }; 519 | actualUpdateCount++; 520 | } 521 | }); 522 | if (isMCP) 523 | logFn.info( 524 | `Applied updates to ${actualUpdateCount} tasks in the dataset.` 525 | ); 526 | else 527 | logFn( 528 | 'info', 529 | `Applied updates to ${actualUpdateCount} tasks in the dataset.` 530 | ); 531 | 532 | // Fix: Pass projectRoot and currentTag to writeJSON 533 | writeJSON(tasksPath, data, projectRoot, tag); 534 | if (isMCP) 535 | logFn.info( 536 | `Successfully updated ${actualUpdateCount} tasks in ${tasksPath}` 537 | ); 538 | else 539 | logFn( 540 | 'success', 541 | `Successfully updated ${actualUpdateCount} tasks in ${tasksPath}` 542 | ); 543 | // await generateTaskFiles(tasksPath, path.dirname(tasksPath)); 544 | 545 | if (outputFormat === 'text' && aiServiceResponse.telemetryData) { 546 | displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); 547 | } 548 | 549 | return { 550 | success: true, 551 | updatedTasks: parsedUpdatedTasks, 552 | telemetryData: aiServiceResponse.telemetryData, 553 | tagInfo: aiServiceResponse.tagInfo 554 | }; 555 | } catch (error) { 556 | if (loadingIndicator) stopLoadingIndicator(loadingIndicator); 557 | if (isMCP) logFn.error(`Error during AI service call: ${error.message}`); 558 | else logFn('error', `Error during AI service call: ${error.message}`); 559 | if (error.message.includes('API key')) { 560 | if (isMCP) 561 | logFn.error( 562 | 'Please ensure API keys are configured correctly in .env or mcp.json.' 563 | ); 564 | else 565 | logFn( 566 | 'error', 567 | 'Please ensure API keys are configured correctly in .env or mcp.json.' 568 | ); 569 | } 570 | throw error; 571 | } finally { 572 | if (loadingIndicator) stopLoadingIndicator(loadingIndicator); 573 | } 574 | } catch (error) { 575 | // --- General Error Handling (Unchanged) --- 576 | if (isMCP) logFn.error(`Error updating tasks: ${error.message}`); 577 | else logFn('error', `Error updating tasks: ${error.message}`); 578 | if (outputFormat === 'text') { 579 | console.error(chalk.red(`Error: ${error.message}`)); 580 | if (getDebugFlag(session)) { 581 | console.error(error); 582 | } 583 | process.exit(1); 584 | } else { 585 | throw error; // Re-throw for MCP/programmatic callers 586 | } 587 | // --- End General Error Handling --- 588 | } 589 | } 590 | 591 | export default updateTasks; 592 | ``` -------------------------------------------------------------------------------- /README-task-master.md: -------------------------------------------------------------------------------- ```markdown 1 | # Task Master 2 | 3 | ### by [@eyaltoledano](https://x.com/eyaltoledano) 4 | 5 | A task management system for AI-driven development with Claude, designed to work seamlessly with Cursor AI. 6 | 7 | ## Requirements 8 | 9 | - Node.js 14.0.0 or higher 10 | - Anthropic API key (Claude API) 11 | - Anthropic SDK version 0.39.0 or higher 12 | - OpenAI SDK (for Perplexity API integration, optional) 13 | 14 | ## Configuration 15 | 16 | Taskmaster uses two primary configuration methods: 17 | 18 | 1. **`.taskmasterconfig` File (Project Root)** 19 | 20 | - Stores most settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default priority/subtasks, project name. 21 | - **Created and managed using `task-master models --setup` CLI command or the `models` MCP tool.** 22 | - Do not edit manually unless you know what you are doing. 23 | 24 | 2. **Environment Variables (`.env` file or MCP `env` block)** 25 | - Used **only** for sensitive **API Keys** (e.g., `ANTHROPIC_API_KEY`, `PERPLEXITY_API_KEY`, etc.) and specific endpoints (like `OLLAMA_BASE_URL`). 26 | - **For CLI:** Place keys in a `.env` file in your project root. 27 | - **For MCP/Cursor:** Place keys in the `env` section of your `.cursor/mcp.json` (or other MCP config according to the AI IDE or client you use) file under the `taskmaster-ai` server definition. 28 | 29 | **Important:** Settings like model choices, max tokens, temperature, and log level are **no longer configured via environment variables.** Use the `task-master models` command or tool. 30 | 31 | See the [Configuration Guide](docs/configuration.md) for full details. 32 | 33 | ## Installation 34 | 35 | ```bash 36 | # Install globally 37 | npm install -g task-master-ai 38 | 39 | # OR install locally within your project 40 | npm install task-master-ai 41 | ``` 42 | 43 | ### Initialize a new project 44 | 45 | ```bash 46 | # If installed globally 47 | task-master init 48 | 49 | # If installed locally 50 | npx task-master init 51 | ``` 52 | 53 | This will prompt you for project details and set up a new project with the necessary files and structure. 54 | 55 | ### Important Notes 56 | 57 | 1. **ES Modules Configuration:** 58 | 59 | - This project uses ES Modules (ESM) instead of CommonJS. 60 | - This is set via `"type": "module"` in your package.json. 61 | - Use `import/export` syntax instead of `require()`. 62 | - Files should use `.js` or `.mjs` extensions. 63 | - To use a CommonJS module, either: 64 | - Rename it with `.cjs` extension 65 | - Use `await import()` for dynamic imports 66 | - If you need CommonJS throughout your project, remove `"type": "module"` from package.json, but Task Master scripts expect ESM. 67 | 68 | 2. The Anthropic SDK version should be 0.39.0 or higher. 69 | 70 | ## Quick Start with Global Commands 71 | 72 | After installing the package globally, you can use these CLI commands from any directory: 73 | 74 | ```bash 75 | # Initialize a new project 76 | task-master init 77 | 78 | # Parse a PRD and generate tasks 79 | task-master parse-prd your-prd.txt 80 | 81 | # List all tasks 82 | task-master list 83 | 84 | # Show the next task to work on 85 | task-master next 86 | 87 | # Generate task files 88 | task-master generate 89 | ``` 90 | 91 | ## Troubleshooting 92 | 93 | ### If `task-master init` doesn't respond: 94 | 95 | Try running it with Node directly: 96 | 97 | ```bash 98 | node node_modules/claude-task-master/scripts/init.js 99 | ``` 100 | 101 | Or clone the repository and run: 102 | 103 | ```bash 104 | git clone https://github.com/eyaltoledano/claude-task-master.git 105 | cd claude-task-master 106 | node scripts/init.js 107 | ``` 108 | 109 | ## Task Structure 110 | 111 | Tasks in tasks.json have the following structure: 112 | 113 | - `id`: Unique identifier for the task (Example: `1`) 114 | - `title`: Brief, descriptive title of the task (Example: `"Initialize Repo"`) 115 | - `description`: Concise description of what the task involves (Example: `"Create a new repository, set up initial structure."`) 116 | - `status`: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`) 117 | - `dependencies`: IDs of tasks that must be completed before this task (Example: `[1, 2]`) 118 | - Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending) 119 | - This helps quickly identify which prerequisite tasks are blocking work 120 | - `priority`: Importance level of the task (Example: `"high"`, `"medium"`, `"low"`) 121 | - `details`: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`) 122 | - `testStrategy`: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`) 123 | - `subtasks`: List of smaller, more specific tasks that make up the main task (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`) 124 | 125 | ## Integrating with Cursor AI 126 | 127 | Claude Task Master is designed to work seamlessly with [Cursor AI](https://www.cursor.so/), providing a structured workflow for AI-driven development. 128 | 129 | ### Setup with Cursor 130 | 131 | 1. After initializing your project, open it in Cursor 132 | 2. The `.cursor/rules/dev_workflow.mdc` file is automatically loaded by Cursor, providing the AI with knowledge about the task management system 133 | 3. Place your PRD document in the `scripts/` directory (e.g., `scripts/prd.txt`) 134 | 4. Open Cursor's AI chat and switch to Agent mode 135 | 136 | ### Setting up MCP in Cursor 137 | 138 | To enable enhanced task management capabilities directly within Cursor using the Model Control Protocol (MCP): 139 | 140 | 1. Go to Cursor settings 141 | 2. Navigate to the MCP section 142 | 3. Click on "Add New MCP Server" 143 | 4. Configure with the following details: 144 | - Name: "Task Master" 145 | - Type: "Command" 146 | - Command: "npx -y task-master-ai" 147 | 5. Save the settings 148 | 149 | Once configured, you can interact with Task Master's task management commands directly through Cursor's interface, providing a more integrated experience. 150 | 151 | ### Initial Task Generation 152 | 153 | In Cursor's AI chat, instruct the agent to generate tasks from your PRD: 154 | 155 | ``` 156 | Please use the task-master parse-prd command to generate tasks from my PRD. The PRD is located at scripts/prd.txt. 157 | ``` 158 | 159 | The agent will execute: 160 | 161 | ```bash 162 | task-master parse-prd scripts/prd.txt 163 | ``` 164 | 165 | This will: 166 | 167 | - Parse your PRD document 168 | - Generate a structured `tasks.json` file with tasks, dependencies, priorities, and test strategies 169 | - The agent will understand this process due to the Cursor rules 170 | 171 | ### Generate Individual Task Files 172 | 173 | Next, ask the agent to generate individual task files: 174 | 175 | ``` 176 | Please generate individual task files from tasks.json 177 | ``` 178 | 179 | The agent will execute: 180 | 181 | ```bash 182 | task-master generate 183 | ``` 184 | 185 | This creates individual task files in the `tasks/` directory (e.g., `task_001.txt`, `task_002.txt`), making it easier to reference specific tasks. 186 | 187 | ## AI-Driven Development Workflow 188 | 189 | The Cursor agent is pre-configured (via the rules file) to follow this workflow: 190 | 191 | ### 1. Task Discovery and Selection 192 | 193 | Ask the agent to list available tasks: 194 | 195 | ``` 196 | What tasks are available to work on next? 197 | ``` 198 | 199 | The agent will: 200 | 201 | - Run `task-master list` to see all tasks 202 | - Run `task-master next` to determine the next task to work on 203 | - Analyze dependencies to determine which tasks are ready to be worked on 204 | - Prioritize tasks based on priority level and ID order 205 | - Suggest the next task(s) to implement 206 | 207 | ### 2. Task Implementation 208 | 209 | When implementing a task, the agent will: 210 | 211 | - Reference the task's details section for implementation specifics 212 | - Consider dependencies on previous tasks 213 | - Follow the project's coding standards 214 | - Create appropriate tests based on the task's testStrategy 215 | 216 | You can ask: 217 | 218 | ``` 219 | Let's implement task 3. What does it involve? 220 | ``` 221 | 222 | ### 3. Task Verification 223 | 224 | Before marking a task as complete, verify it according to: 225 | 226 | - The task's specified testStrategy 227 | - Any automated tests in the codebase 228 | - Manual verification if required 229 | 230 | ### 4. Task Completion 231 | 232 | When a task is completed, tell the agent: 233 | 234 | ``` 235 | Task 3 is now complete. Please update its status. 236 | ``` 237 | 238 | The agent will execute: 239 | 240 | ```bash 241 | task-master set-status --id=3 --status=done 242 | ``` 243 | 244 | ### 5. Handling Implementation Drift 245 | 246 | If during implementation, you discover that: 247 | 248 | - The current approach differs significantly from what was planned 249 | - Future tasks need to be modified due to current implementation choices 250 | - New dependencies or requirements have emerged 251 | 252 | Tell the agent: 253 | 254 | ``` 255 | We've changed our approach. We're now using Express instead of Fastify. Please update all future tasks to reflect this change. 256 | ``` 257 | 258 | The agent will execute: 259 | 260 | ```bash 261 | task-master update --from=4 --prompt="Now we are using Express instead of Fastify." 262 | ``` 263 | 264 | This will rewrite or re-scope subsequent tasks in tasks.json while preserving completed work. 265 | 266 | ### 6. Breaking Down Complex Tasks 267 | 268 | For complex tasks that need more granularity: 269 | 270 | ``` 271 | Task 5 seems complex. Can you break it down into subtasks? 272 | ``` 273 | 274 | The agent will execute: 275 | 276 | ```bash 277 | task-master expand --id=5 --num=3 278 | ``` 279 | 280 | You can provide additional context: 281 | 282 | ``` 283 | Please break down task 5 with a focus on security considerations. 284 | ``` 285 | 286 | The agent will execute: 287 | 288 | ```bash 289 | task-master expand --id=5 --prompt="Focus on security aspects" 290 | ``` 291 | 292 | You can also expand all pending tasks: 293 | 294 | ``` 295 | Please break down all pending tasks into subtasks. 296 | ``` 297 | 298 | The agent will execute: 299 | 300 | ```bash 301 | task-master expand --all 302 | ``` 303 | 304 | For research-backed subtask generation using Perplexity AI: 305 | 306 | ``` 307 | Please break down task 5 using research-backed generation. 308 | ``` 309 | 310 | The agent will execute: 311 | 312 | ```bash 313 | task-master expand --id=5 --research 314 | ``` 315 | 316 | ## Command Reference 317 | 318 | Here's a comprehensive reference of all available commands: 319 | 320 | ### Parse PRD 321 | 322 | ```bash 323 | # Parse a PRD file and generate tasks 324 | task-master parse-prd <prd-file.txt> 325 | 326 | # Limit the number of tasks generated (default is 10) 327 | task-master parse-prd <prd-file.txt> --num-tasks=5 328 | 329 | # Allow task master to determine the number of tasks based on complexity 330 | task-master parse-prd <prd-file.txt> --num-tasks=0 331 | ``` 332 | 333 | ### List Tasks 334 | 335 | ```bash 336 | # List all tasks 337 | task-master list 338 | 339 | # List tasks with a specific status 340 | task-master list --status=<status> 341 | 342 | # List tasks with subtasks 343 | task-master list --with-subtasks 344 | 345 | # List tasks with a specific status and include subtasks 346 | task-master list --status=<status> --with-subtasks 347 | ``` 348 | 349 | ### Show Next Task 350 | 351 | ```bash 352 | # Show the next task to work on based on dependencies and status 353 | task-master next 354 | ``` 355 | 356 | ### Show Specific Task 357 | 358 | ```bash 359 | # Show details of a specific task 360 | task-master show <id> 361 | # or 362 | task-master show --id=<id> 363 | 364 | # View a specific subtask (e.g., subtask 2 of task 1) 365 | task-master show 1.2 366 | ``` 367 | 368 | ### Update Tasks 369 | 370 | ```bash 371 | # Update tasks from a specific ID and provide context 372 | task-master update --from=<id> --prompt="<prompt>" 373 | ``` 374 | 375 | ### Generate Task Files 376 | 377 | ```bash 378 | # Generate individual task files from tasks.json 379 | task-master generate 380 | ``` 381 | 382 | ### Set Task Status 383 | 384 | ```bash 385 | # Set status of a single task 386 | task-master set-status --id=<id> --status=<status> 387 | 388 | # Set status for multiple tasks 389 | task-master set-status --id=1,2,3 --status=<status> 390 | 391 | # Set status for subtasks 392 | task-master set-status --id=1.1,1.2 --status=<status> 393 | ``` 394 | 395 | When marking a task as "done", all of its subtasks will automatically be marked as "done" as well. 396 | 397 | ### Expand Tasks 398 | 399 | ```bash 400 | # Expand a specific task with subtasks 401 | task-master expand --id=<id> --num=<number> 402 | 403 | # Expand a task with a dynamic number of subtasks (ignoring complexity report) 404 | task-master expand --id=<id> --num=0 405 | 406 | # Expand with additional context 407 | task-master expand --id=<id> --prompt="<context>" 408 | 409 | # Expand all pending tasks 410 | task-master expand --all 411 | 412 | # Force regeneration of subtasks for tasks that already have them 413 | task-master expand --all --force 414 | 415 | # Research-backed subtask generation for a specific task 416 | task-master expand --id=<id> --research 417 | 418 | # Research-backed generation for all tasks 419 | task-master expand --all --research 420 | ``` 421 | 422 | ### Clear Subtasks 423 | 424 | ```bash 425 | # Clear subtasks from a specific task 426 | task-master clear-subtasks --id=<id> 427 | 428 | # Clear subtasks from multiple tasks 429 | task-master clear-subtasks --id=1,2,3 430 | 431 | # Clear subtasks from all tasks 432 | task-master clear-subtasks --all 433 | ``` 434 | 435 | ### Analyze Task Complexity 436 | 437 | ```bash 438 | # Analyze complexity of all tasks 439 | task-master analyze-complexity 440 | 441 | # Save report to a custom location 442 | task-master analyze-complexity --output=my-report.json 443 | 444 | # Use a specific LLM model 445 | task-master analyze-complexity --model=claude-3-opus-20240229 446 | 447 | # Set a custom complexity threshold (1-10) 448 | task-master analyze-complexity --threshold=6 449 | 450 | # Use an alternative tasks file 451 | task-master analyze-complexity --file=custom-tasks.json 452 | 453 | # Use Perplexity AI for research-backed complexity analysis 454 | task-master analyze-complexity --research 455 | ``` 456 | 457 | ### View Complexity Report 458 | 459 | ```bash 460 | # Display the task complexity analysis report 461 | task-master complexity-report 462 | 463 | # View a report at a custom location 464 | task-master complexity-report --file=my-report.json 465 | ``` 466 | 467 | ### Managing Task Dependencies 468 | 469 | ```bash 470 | # Add a dependency to a task 471 | task-master add-dependency --id=<id> --depends-on=<id> 472 | 473 | # Remove a dependency from a task 474 | task-master remove-dependency --id=<id> --depends-on=<id> 475 | 476 | # Validate dependencies without fixing them 477 | task-master validate-dependencies 478 | 479 | # Find and fix invalid dependencies automatically 480 | task-master fix-dependencies 481 | ``` 482 | 483 | ### Add a New Task 484 | 485 | ```bash 486 | # Add a new task using AI 487 | task-master add-task --prompt="Description of the new task" 488 | 489 | # Add a task with dependencies 490 | task-master add-task --prompt="Description" --dependencies=1,2,3 491 | 492 | # Add a task with priority 493 | task-master add-task --prompt="Description" --priority=high 494 | ``` 495 | 496 | ## Feature Details 497 | 498 | ### Analyzing Task Complexity 499 | 500 | The `analyze-complexity` command: 501 | 502 | - Analyzes each task using AI to assess its complexity on a scale of 1-10 503 | - Recommends optimal number of subtasks based on configured DEFAULT_SUBTASKS 504 | - Generates tailored prompts for expanding each task 505 | - Creates a comprehensive JSON report with ready-to-use commands 506 | - Saves the report to scripts/task-complexity-report.json by default 507 | 508 | The generated report contains: 509 | 510 | - Complexity analysis for each task (scored 1-10) 511 | - Recommended number of subtasks based on complexity 512 | - AI-generated expansion prompts customized for each task 513 | - Ready-to-run expansion commands directly within each task analysis 514 | 515 | ### Viewing Complexity Report 516 | 517 | The `complexity-report` command: 518 | 519 | - Displays a formatted, easy-to-read version of the complexity analysis report 520 | - Shows tasks organized by complexity score (highest to lowest) 521 | - Provides complexity distribution statistics (low, medium, high) 522 | - Highlights tasks recommended for expansion based on threshold score 523 | - Includes ready-to-use expansion commands for each complex task 524 | - If no report exists, offers to generate one on the spot 525 | 526 | ### Smart Task Expansion 527 | 528 | The `expand` command automatically checks for and uses the complexity report: 529 | 530 | When a complexity report exists: 531 | 532 | - Tasks are automatically expanded using the recommended subtask count and prompts 533 | - When expanding all tasks, they're processed in order of complexity (highest first) 534 | - Research-backed generation is preserved from the complexity analysis 535 | - You can still override recommendations with explicit command-line options 536 | 537 | Example workflow: 538 | 539 | ```bash 540 | # Generate the complexity analysis report with research capabilities 541 | task-master analyze-complexity --research 542 | 543 | # Review the report in a readable format 544 | task-master complexity-report 545 | 546 | # Expand tasks using the optimized recommendations 547 | task-master expand --id=8 548 | # or expand all tasks 549 | task-master expand --all 550 | ``` 551 | 552 | ### Finding the Next Task 553 | 554 | The `next` command: 555 | 556 | - Identifies tasks that are pending/in-progress and have all dependencies satisfied 557 | - Prioritizes tasks by priority level, dependency count, and task ID 558 | - Displays comprehensive information about the selected task: 559 | - Basic task details (ID, title, priority, dependencies) 560 | - Implementation details 561 | - Subtasks (if they exist) 562 | - Provides contextual suggested actions: 563 | - Command to mark the task as in-progress 564 | - Command to mark the task as done 565 | - Commands for working with subtasks 566 | 567 | ### Viewing Specific Task Details 568 | 569 | The `show` command: 570 | 571 | - Displays comprehensive details about a specific task or subtask 572 | - Shows task status, priority, dependencies, and detailed implementation notes 573 | - For parent tasks, displays all subtasks and their status 574 | - For subtasks, shows parent task relationship 575 | - Provides contextual action suggestions based on the task's state 576 | - Works with both regular tasks and subtasks (using the format taskId.subtaskId) 577 | 578 | ## Best Practices for AI-Driven Development 579 | 580 | 1. **Start with a detailed PRD**: The more detailed your PRD, the better the generated tasks will be. 581 | 582 | 2. **Review generated tasks**: After parsing the PRD, review the tasks to ensure they make sense and have appropriate dependencies. 583 | 584 | 3. **Analyze task complexity**: Use the complexity analysis feature to identify which tasks should be broken down further. 585 | 586 | 4. **Follow the dependency chain**: Always respect task dependencies - the Cursor agent will help with this. 587 | 588 | 5. **Update as you go**: If your implementation diverges from the plan, use the update command to keep future tasks aligned with your current approach. 589 | 590 | 6. **Break down complex tasks**: Use the expand command to break down complex tasks into manageable subtasks. 591 | 592 | 7. **Regenerate task files**: After any updates to tasks.json, regenerate the task files to keep them in sync. 593 | 594 | 8. **Communicate context to the agent**: When asking the Cursor agent to help with a task, provide context about what you're trying to achieve. 595 | 596 | 9. **Validate dependencies**: Periodically run the validate-dependencies command to check for invalid or circular dependencies. 597 | 598 | ## Example Cursor AI Interactions 599 | 600 | ### Starting a new project 601 | 602 | ``` 603 | I've just initialized a new project with Claude Task Master. I have a PRD at scripts/prd.txt. 604 | Can you help me parse it and set up the initial tasks? 605 | ``` 606 | 607 | ### Working on tasks 608 | 609 | ``` 610 | What's the next task I should work on? Please consider dependencies and priorities. 611 | ``` 612 | 613 | ### Implementing a specific task 614 | 615 | ``` 616 | I'd like to implement task 4. Can you help me understand what needs to be done and how to approach it? 617 | ``` 618 | 619 | ### Managing subtasks 620 | 621 | ``` 622 | I need to regenerate the subtasks for task 3 with a different approach. Can you help me clear and regenerate them? 623 | ``` 624 | 625 | ### Handling changes 626 | 627 | ``` 628 | We've decided to use MongoDB instead of PostgreSQL. Can you update all future tasks to reflect this change? 629 | ``` 630 | 631 | ### Completing work 632 | 633 | ``` 634 | I've finished implementing the authentication system described in task 2. All tests are passing. 635 | Please mark it as complete and tell me what I should work on next. 636 | ``` 637 | 638 | ### Analyzing complexity 639 | 640 | ``` 641 | Can you analyze the complexity of our tasks to help me understand which ones need to be broken down further? 642 | ``` 643 | 644 | ### Viewing complexity report 645 | 646 | ``` 647 | Can you show me the complexity report in a more readable format? 648 | ``` 649 | ```