This is page 13 of 38. Use http://codebase.md/eyaltoledano/claude-task-master?page={x} to view the full context. # Directory Structure ``` ├── .changeset │ ├── config.json │ └── README.md ├── .claude │ ├── agents │ │ ├── task-checker.md │ │ ├── task-executor.md │ │ └── task-orchestrator.md │ ├── commands │ │ ├── dedupe.md │ │ └── tm │ │ ├── add-dependency │ │ │ └── add-dependency.md │ │ ├── add-subtask │ │ │ ├── add-subtask.md │ │ │ └── convert-task-to-subtask.md │ │ ├── add-task │ │ │ └── add-task.md │ │ ├── analyze-complexity │ │ │ └── analyze-complexity.md │ │ ├── complexity-report │ │ │ └── complexity-report.md │ │ ├── expand │ │ │ ├── expand-all-tasks.md │ │ │ └── expand-task.md │ │ ├── fix-dependencies │ │ │ └── fix-dependencies.md │ │ ├── generate │ │ │ └── generate-tasks.md │ │ ├── help.md │ │ ├── init │ │ │ ├── init-project-quick.md │ │ │ └── init-project.md │ │ ├── learn.md │ │ ├── list │ │ │ ├── list-tasks-by-status.md │ │ │ ├── list-tasks-with-subtasks.md │ │ │ └── list-tasks.md │ │ ├── models │ │ │ ├── setup-models.md │ │ │ └── view-models.md │ │ ├── next │ │ │ └── next-task.md │ │ ├── parse-prd │ │ │ ├── parse-prd-with-research.md │ │ │ └── parse-prd.md │ │ ├── remove-dependency │ │ │ └── remove-dependency.md │ │ ├── remove-subtask │ │ │ └── remove-subtask.md │ │ ├── remove-subtasks │ │ │ ├── remove-all-subtasks.md │ │ │ └── remove-subtasks.md │ │ ├── remove-task │ │ │ └── remove-task.md │ │ ├── set-status │ │ │ ├── to-cancelled.md │ │ │ ├── to-deferred.md │ │ │ ├── to-done.md │ │ │ ├── to-in-progress.md │ │ │ ├── to-pending.md │ │ │ └── to-review.md │ │ ├── setup │ │ │ ├── install-taskmaster.md │ │ │ └── quick-install-taskmaster.md │ │ ├── show │ │ │ └── show-task.md │ │ ├── status │ │ │ └── project-status.md │ │ ├── sync-readme │ │ │ └── sync-readme.md │ │ ├── tm-main.md │ │ ├── update │ │ │ ├── update-single-task.md │ │ │ ├── update-task.md │ │ │ └── update-tasks-from-id.md │ │ ├── utils │ │ │ └── analyze-project.md │ │ ├── validate-dependencies │ │ │ └── validate-dependencies.md │ │ └── workflows │ │ ├── auto-implement-tasks.md │ │ ├── command-pipeline.md │ │ └── smart-workflow.md │ └── TM_COMMANDS_GUIDE.md ├── .coderabbit.yaml ├── .cursor │ ├── mcp.json │ └── rules │ ├── ai_providers.mdc │ ├── ai_services.mdc │ ├── architecture.mdc │ ├── changeset.mdc │ ├── commands.mdc │ ├── context_gathering.mdc │ ├── cursor_rules.mdc │ ├── dependencies.mdc │ ├── dev_workflow.mdc │ ├── git_workflow.mdc │ ├── glossary.mdc │ ├── mcp.mdc │ ├── new_features.mdc │ ├── self_improve.mdc │ ├── tags.mdc │ ├── taskmaster.mdc │ ├── tasks.mdc │ ├── telemetry.mdc │ ├── test_workflow.mdc │ ├── tests.mdc │ ├── ui.mdc │ └── utilities.mdc ├── .cursorignore ├── .env.example ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── enhancements---feature-requests.md │ │ └── feedback.md │ ├── PULL_REQUEST_TEMPLATE │ │ ├── bugfix.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── integration.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── scripts │ │ ├── auto-close-duplicates.mjs │ │ ├── backfill-duplicate-comments.mjs │ │ ├── check-pre-release-mode.mjs │ │ ├── parse-metrics.mjs │ │ ├── release.mjs │ │ ├── tag-extension.mjs │ │ └── utils.mjs │ └── workflows │ ├── auto-close-duplicates.yml │ ├── backfill-duplicate-comments.yml │ ├── ci.yml │ ├── claude-dedupe-issues.yml │ ├── claude-docs-trigger.yml │ ├── claude-docs-updater.yml │ ├── claude-issue-triage.yml │ ├── claude.yml │ ├── extension-ci.yml │ ├── extension-release.yml │ ├── log-issue-events.yml │ ├── pre-release.yml │ ├── release-check.yml │ ├── release.yml │ ├── update-models-md.yml │ └── weekly-metrics-discord.yml ├── .gitignore ├── .kiro │ ├── hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── settings │ │ └── mcp.json │ └── steering │ ├── dev_workflow.md │ ├── kiro_rules.md │ ├── self_improve.md │ ├── taskmaster_hooks_workflow.md │ └── taskmaster.md ├── .manypkg.json ├── .mcp.json ├── .npmignore ├── .nvmrc ├── .taskmaster │ ├── CLAUDE.md │ ├── config.json │ ├── docs │ │ ├── MIGRATION-ROADMAP.md │ │ ├── prd-tm-start.txt │ │ ├── prd.txt │ │ ├── README.md │ │ ├── research │ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md │ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md │ │ │ ├── 2025-06-14_test-save-functionality.md │ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md │ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md │ │ ├── task-template-importing-prd.txt │ │ ├── test-prd.txt │ │ └── tm-core-phase-1.txt │ ├── reports │ │ ├── task-complexity-report_cc-kiro-hooks.json │ │ ├── task-complexity-report_test-prd-tag.json │ │ ├── task-complexity-report_tm-core-phase-1.json │ │ ├── task-complexity-report.json │ │ └── tm-core-complexity.json │ ├── state.json │ ├── tasks │ │ ├── task_001_tm-start.txt │ │ ├── task_002_tm-start.txt │ │ ├── task_003_tm-start.txt │ │ ├── task_004_tm-start.txt │ │ ├── task_007_tm-start.txt │ │ └── tasks.json │ └── templates │ └── example_prd.txt ├── .vscode │ ├── extensions.json │ └── settings.json ├── apps │ ├── cli │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ ├── commands │ │ │ │ ├── auth.command.ts │ │ │ │ ├── context.command.ts │ │ │ │ ├── list.command.ts │ │ │ │ ├── set-status.command.ts │ │ │ │ ├── show.command.ts │ │ │ │ └── start.command.ts │ │ │ ├── index.ts │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── dashboard.component.ts │ │ │ │ │ ├── header.component.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── next-task.component.ts │ │ │ │ │ ├── suggested-steps.component.ts │ │ │ │ │ └── task-detail.component.ts │ │ │ │ └── index.ts │ │ │ └── utils │ │ │ ├── auto-update.ts │ │ │ └── ui.ts │ │ └── tsconfig.json │ ├── docs │ │ ├── archive │ │ │ ├── ai-client-utils-example.mdx │ │ │ ├── ai-development-workflow.mdx │ │ │ ├── command-reference.mdx │ │ │ ├── configuration.mdx │ │ │ ├── cursor-setup.mdx │ │ │ ├── examples.mdx │ │ │ └── Installation.mdx │ │ ├── best-practices │ │ │ ├── advanced-tasks.mdx │ │ │ ├── configuration-advanced.mdx │ │ │ └── index.mdx │ │ ├── capabilities │ │ │ ├── cli-root-commands.mdx │ │ │ ├── index.mdx │ │ │ ├── mcp.mdx │ │ │ └── task-structure.mdx │ │ ├── CHANGELOG.md │ │ ├── docs.json │ │ ├── favicon.svg │ │ ├── getting-started │ │ │ ├── contribute.mdx │ │ │ ├── faq.mdx │ │ │ └── quick-start │ │ │ ├── configuration-quick.mdx │ │ │ ├── execute-quick.mdx │ │ │ ├── installation.mdx │ │ │ ├── moving-forward.mdx │ │ │ ├── prd-quick.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── requirements.mdx │ │ │ ├── rules-quick.mdx │ │ │ └── tasks-quick.mdx │ │ ├── introduction.mdx │ │ ├── licensing.md │ │ ├── logo │ │ │ ├── dark.svg │ │ │ ├── light.svg │ │ │ └── task-master-logo.png │ │ ├── package.json │ │ ├── README.md │ │ ├── style.css │ │ ├── vercel.json │ │ └── whats-new.mdx │ └── extension │ ├── .vscodeignore │ ├── assets │ │ ├── banner.png │ │ ├── icon-dark.svg │ │ ├── icon-light.svg │ │ ├── icon.png │ │ ├── screenshots │ │ │ ├── kanban-board.png │ │ │ └── task-details.png │ │ └── sidebar-icon.svg │ ├── CHANGELOG.md │ ├── components.json │ ├── docs │ │ ├── extension-CI-setup.md │ │ └── extension-development-guide.md │ ├── esbuild.js │ ├── LICENSE │ ├── package.json │ ├── package.mjs │ ├── package.publish.json │ ├── README.md │ ├── src │ │ ├── components │ │ │ ├── ConfigView.tsx │ │ │ ├── constants.ts │ │ │ ├── TaskDetails │ │ │ │ ├── AIActionsSection.tsx │ │ │ │ ├── DetailsSection.tsx │ │ │ │ ├── PriorityBadge.tsx │ │ │ │ ├── SubtasksSection.tsx │ │ │ │ ├── TaskMetadataSidebar.tsx │ │ │ │ └── useTaskDetails.ts │ │ │ ├── TaskDetailsView.tsx │ │ │ ├── TaskMasterLogo.tsx │ │ │ └── ui │ │ │ ├── badge.tsx │ │ │ ├── breadcrumb.tsx │ │ │ ├── button.tsx │ │ │ ├── card.tsx │ │ │ ├── collapsible.tsx │ │ │ ├── CollapsibleSection.tsx │ │ │ ├── dropdown-menu.tsx │ │ │ ├── label.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── separator.tsx │ │ │ ├── shadcn-io │ │ │ │ └── kanban │ │ │ │ └── index.tsx │ │ │ └── textarea.tsx │ │ ├── extension.ts │ │ ├── index.ts │ │ ├── lib │ │ │ └── utils.ts │ │ ├── services │ │ │ ├── config-service.ts │ │ │ ├── error-handler.ts │ │ │ ├── notification-preferences.ts │ │ │ ├── polling-service.ts │ │ │ ├── polling-strategies.ts │ │ │ ├── sidebar-webview-manager.ts │ │ │ ├── task-repository.ts │ │ │ ├── terminal-manager.ts │ │ │ └── webview-manager.ts │ │ ├── test │ │ │ └── extension.test.ts │ │ ├── utils │ │ │ ├── configManager.ts │ │ │ ├── connectionManager.ts │ │ │ ├── errorHandler.ts │ │ │ ├── event-emitter.ts │ │ │ ├── logger.ts │ │ │ ├── mcpClient.ts │ │ │ ├── notificationPreferences.ts │ │ │ └── task-master-api │ │ │ ├── cache │ │ │ │ └── cache-manager.ts │ │ │ ├── index.ts │ │ │ ├── mcp-client.ts │ │ │ ├── transformers │ │ │ │ └── task-transformer.ts │ │ │ └── types │ │ │ └── index.ts │ │ └── webview │ │ ├── App.tsx │ │ ├── components │ │ │ ├── AppContent.tsx │ │ │ ├── EmptyState.tsx │ │ │ ├── ErrorBoundary.tsx │ │ │ ├── PollingStatus.tsx │ │ │ ├── PriorityBadge.tsx │ │ │ ├── SidebarView.tsx │ │ │ ├── TagDropdown.tsx │ │ │ ├── TaskCard.tsx │ │ │ ├── TaskEditModal.tsx │ │ │ ├── TaskMasterKanban.tsx │ │ │ ├── ToastContainer.tsx │ │ │ └── ToastNotification.tsx │ │ ├── constants │ │ │ └── index.ts │ │ ├── contexts │ │ │ └── VSCodeContext.tsx │ │ ├── hooks │ │ │ ├── useTaskQueries.ts │ │ │ ├── useVSCodeMessages.ts │ │ │ └── useWebviewHeight.ts │ │ ├── index.css │ │ ├── index.tsx │ │ ├── providers │ │ │ └── QueryProvider.tsx │ │ ├── reducers │ │ │ └── appReducer.ts │ │ ├── sidebar.tsx │ │ ├── types │ │ │ └── index.ts │ │ └── utils │ │ ├── logger.ts │ │ └── toast.ts │ └── tsconfig.json ├── assets │ ├── .windsurfrules │ ├── AGENTS.md │ ├── claude │ │ ├── agents │ │ │ ├── task-checker.md │ │ │ ├── task-executor.md │ │ │ └── task-orchestrator.md │ │ ├── commands │ │ │ └── tm │ │ │ ├── add-dependency │ │ │ │ └── add-dependency.md │ │ │ ├── add-subtask │ │ │ │ ├── add-subtask.md │ │ │ │ └── convert-task-to-subtask.md │ │ │ ├── add-task │ │ │ │ └── add-task.md │ │ │ ├── analyze-complexity │ │ │ │ └── analyze-complexity.md │ │ │ ├── clear-subtasks │ │ │ │ ├── clear-all-subtasks.md │ │ │ │ └── clear-subtasks.md │ │ │ ├── complexity-report │ │ │ │ └── complexity-report.md │ │ │ ├── expand │ │ │ │ ├── expand-all-tasks.md │ │ │ │ └── expand-task.md │ │ │ ├── fix-dependencies │ │ │ │ └── fix-dependencies.md │ │ │ ├── generate │ │ │ │ └── generate-tasks.md │ │ │ ├── help.md │ │ │ ├── init │ │ │ │ ├── init-project-quick.md │ │ │ │ └── init-project.md │ │ │ ├── learn.md │ │ │ ├── list │ │ │ │ ├── list-tasks-by-status.md │ │ │ │ ├── list-tasks-with-subtasks.md │ │ │ │ └── list-tasks.md │ │ │ ├── models │ │ │ │ ├── setup-models.md │ │ │ │ └── view-models.md │ │ │ ├── next │ │ │ │ └── next-task.md │ │ │ ├── parse-prd │ │ │ │ ├── parse-prd-with-research.md │ │ │ │ └── parse-prd.md │ │ │ ├── remove-dependency │ │ │ │ └── remove-dependency.md │ │ │ ├── remove-subtask │ │ │ │ └── remove-subtask.md │ │ │ ├── remove-subtasks │ │ │ │ ├── remove-all-subtasks.md │ │ │ │ └── remove-subtasks.md │ │ │ ├── remove-task │ │ │ │ └── remove-task.md │ │ │ ├── set-status │ │ │ │ ├── to-cancelled.md │ │ │ │ ├── to-deferred.md │ │ │ │ ├── to-done.md │ │ │ │ ├── to-in-progress.md │ │ │ │ ├── to-pending.md │ │ │ │ └── to-review.md │ │ │ ├── setup │ │ │ │ ├── install-taskmaster.md │ │ │ │ └── quick-install-taskmaster.md │ │ │ ├── show │ │ │ │ └── show-task.md │ │ │ ├── status │ │ │ │ └── project-status.md │ │ │ ├── sync-readme │ │ │ │ └── sync-readme.md │ │ │ ├── tm-main.md │ │ │ ├── update │ │ │ │ ├── update-single-task.md │ │ │ │ ├── update-task.md │ │ │ │ └── update-tasks-from-id.md │ │ │ ├── utils │ │ │ │ └── analyze-project.md │ │ │ ├── validate-dependencies │ │ │ │ └── validate-dependencies.md │ │ │ └── workflows │ │ │ ├── auto-implement-tasks.md │ │ │ ├── command-pipeline.md │ │ │ └── smart-workflow.md │ │ └── TM_COMMANDS_GUIDE.md │ ├── config.json │ ├── env.example │ ├── example_prd.txt │ ├── gitignore │ ├── kiro-hooks │ │ ├── tm-code-change-task-tracker.kiro.hook │ │ ├── tm-complexity-analyzer.kiro.hook │ │ ├── tm-daily-standup-assistant.kiro.hook │ │ ├── tm-git-commit-task-linker.kiro.hook │ │ ├── tm-pr-readiness-checker.kiro.hook │ │ ├── tm-task-dependency-auto-progression.kiro.hook │ │ └── tm-test-success-task-completer.kiro.hook │ ├── roocode │ │ ├── .roo │ │ │ ├── rules-architect │ │ │ │ └── architect-rules │ │ │ ├── rules-ask │ │ │ │ └── ask-rules │ │ │ ├── rules-code │ │ │ │ └── code-rules │ │ │ ├── rules-debug │ │ │ │ └── debug-rules │ │ │ ├── rules-orchestrator │ │ │ │ └── orchestrator-rules │ │ │ └── rules-test │ │ │ └── test-rules │ │ └── .roomodes │ ├── rules │ │ ├── cursor_rules.mdc │ │ ├── dev_workflow.mdc │ │ ├── self_improve.mdc │ │ ├── taskmaster_hooks_workflow.mdc │ │ └── taskmaster.mdc │ └── scripts_README.md ├── bin │ └── task-master.js ├── biome.json ├── CHANGELOG.md ├── CLAUDE.md ├── context │ ├── chats │ │ ├── add-task-dependencies-1.md │ │ └── max-min-tokens.txt.md │ ├── fastmcp-core.txt │ ├── fastmcp-docs.txt │ ├── MCP_INTEGRATION.md │ ├── mcp-js-sdk-docs.txt │ ├── mcp-protocol-repo.txt │ ├── mcp-protocol-schema-03262025.json │ └── mcp-protocol-spec.txt ├── CONTRIBUTING.md ├── docs │ ├── CLI-COMMANDER-PATTERN.md │ ├── command-reference.md │ ├── configuration.md │ ├── contributor-docs │ │ └── testing-roo-integration.md │ ├── cross-tag-task-movement.md │ ├── examples │ │ └── claude-code-usage.md │ ├── examples.md │ ├── licensing.md │ ├── mcp-provider-guide.md │ ├── mcp-provider.md │ ├── migration-guide.md │ ├── models.md │ ├── providers │ │ └── gemini-cli.md │ ├── README.md │ ├── scripts │ │ └── models-json-to-markdown.js │ ├── task-structure.md │ └── tutorial.md ├── images │ └── logo.png ├── index.js ├── jest.config.js ├── jest.resolver.cjs ├── LICENSE ├── llms-install.md ├── mcp-server │ ├── server.js │ └── src │ ├── core │ │ ├── __tests__ │ │ │ └── context-manager.test.js │ │ ├── context-manager.js │ │ ├── direct-functions │ │ │ ├── add-dependency.js │ │ │ ├── add-subtask.js │ │ │ ├── add-tag.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── cache-stats.js │ │ │ ├── clear-subtasks.js │ │ │ ├── complexity-report.js │ │ │ ├── copy-tag.js │ │ │ ├── create-tag-from-branch.js │ │ │ ├── delete-tag.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── fix-dependencies.js │ │ │ ├── generate-task-files.js │ │ │ ├── initialize-project.js │ │ │ ├── list-tags.js │ │ │ ├── list-tasks.js │ │ │ ├── models.js │ │ │ ├── move-task-cross-tag.js │ │ │ ├── move-task.js │ │ │ ├── next-task.js │ │ │ ├── parse-prd.js │ │ │ ├── remove-dependency.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── rename-tag.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── rules.js │ │ │ ├── scope-down.js │ │ │ ├── scope-up.js │ │ │ ├── set-task-status.js │ │ │ ├── show-task.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ ├── update-tasks.js │ │ │ ├── use-tag.js │ │ │ └── validate-dependencies.js │ │ ├── task-master-core.js │ │ └── utils │ │ ├── env-utils.js │ │ └── path-utils.js │ ├── custom-sdk │ │ ├── errors.js │ │ ├── index.js │ │ ├── json-extractor.js │ │ ├── language-model.js │ │ ├── message-converter.js │ │ └── schema-converter.js │ ├── index.js │ ├── logger.js │ ├── providers │ │ └── mcp-provider.js │ └── tools │ ├── add-dependency.js │ ├── add-subtask.js │ ├── add-tag.js │ ├── add-task.js │ ├── analyze.js │ ├── clear-subtasks.js │ ├── complexity-report.js │ ├── copy-tag.js │ ├── delete-tag.js │ ├── expand-all.js │ ├── expand-task.js │ ├── fix-dependencies.js │ ├── generate.js │ ├── get-operation-status.js │ ├── get-task.js │ ├── get-tasks.js │ ├── index.js │ ├── initialize-project.js │ ├── list-tags.js │ ├── models.js │ ├── move-task.js │ ├── next-task.js │ ├── parse-prd.js │ ├── remove-dependency.js │ ├── remove-subtask.js │ ├── remove-task.js │ ├── rename-tag.js │ ├── research.js │ ├── response-language.js │ ├── rules.js │ ├── scope-down.js │ ├── scope-up.js │ ├── set-task-status.js │ ├── update-subtask.js │ ├── update-task.js │ ├── update.js │ ├── use-tag.js │ ├── utils.js │ └── validate-dependencies.js ├── mcp-test.js ├── output.json ├── package-lock.json ├── package.json ├── packages │ ├── build-config │ │ ├── CHANGELOG.md │ │ ├── package.json │ │ ├── src │ │ │ └── tsdown.base.ts │ │ └── tsconfig.json │ └── tm-core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── docs │ │ └── listTasks-architecture.md │ ├── package.json │ ├── POC-STATUS.md │ ├── README.md │ ├── src │ │ ├── auth │ │ │ ├── auth-manager.test.ts │ │ │ ├── auth-manager.ts │ │ │ ├── config.ts │ │ │ ├── credential-store.test.ts │ │ │ ├── credential-store.ts │ │ │ ├── index.ts │ │ │ ├── oauth-service.ts │ │ │ ├── supabase-session-storage.ts │ │ │ └── types.ts │ │ ├── clients │ │ │ ├── index.ts │ │ │ └── supabase-client.ts │ │ ├── config │ │ │ ├── config-manager.spec.ts │ │ │ ├── config-manager.ts │ │ │ ├── index.ts │ │ │ └── services │ │ │ ├── config-loader.service.spec.ts │ │ │ ├── config-loader.service.ts │ │ │ ├── config-merger.service.spec.ts │ │ │ ├── config-merger.service.ts │ │ │ ├── config-persistence.service.spec.ts │ │ │ ├── config-persistence.service.ts │ │ │ ├── environment-config-provider.service.spec.ts │ │ │ ├── environment-config-provider.service.ts │ │ │ ├── index.ts │ │ │ ├── runtime-state-manager.service.spec.ts │ │ │ └── runtime-state-manager.service.ts │ │ ├── constants │ │ │ └── index.ts │ │ ├── entities │ │ │ └── task.entity.ts │ │ ├── errors │ │ │ ├── index.ts │ │ │ └── task-master-error.ts │ │ ├── executors │ │ │ ├── base-executor.ts │ │ │ ├── claude-executor.ts │ │ │ ├── executor-factory.ts │ │ │ ├── executor-service.ts │ │ │ ├── index.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ ├── interfaces │ │ │ ├── ai-provider.interface.ts │ │ │ ├── configuration.interface.ts │ │ │ ├── index.ts │ │ │ └── storage.interface.ts │ │ ├── logger │ │ │ ├── factory.ts │ │ │ ├── index.ts │ │ │ └── logger.ts │ │ ├── mappers │ │ │ └── TaskMapper.ts │ │ ├── parser │ │ │ └── index.ts │ │ ├── providers │ │ │ ├── ai │ │ │ │ ├── base-provider.ts │ │ │ │ └── index.ts │ │ │ └── index.ts │ │ ├── repositories │ │ │ ├── supabase-task-repository.ts │ │ │ └── task-repository.interface.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ ├── organization.service.ts │ │ │ ├── task-execution-service.ts │ │ │ └── task-service.ts │ │ ├── storage │ │ │ ├── api-storage.ts │ │ │ ├── file-storage │ │ │ │ ├── file-operations.ts │ │ │ │ ├── file-storage.ts │ │ │ │ ├── format-handler.ts │ │ │ │ ├── index.ts │ │ │ │ └── path-resolver.ts │ │ │ ├── index.ts │ │ │ └── storage-factory.ts │ │ ├── subpath-exports.test.ts │ │ ├── task-master-core.ts │ │ ├── types │ │ │ ├── database.types.ts │ │ │ ├── index.ts │ │ │ └── legacy.ts │ │ └── utils │ │ ├── id-generator.ts │ │ └── index.ts │ ├── tests │ │ ├── integration │ │ │ └── list-tasks.test.ts │ │ ├── mocks │ │ │ └── mock-provider.ts │ │ ├── setup.ts │ │ └── unit │ │ ├── base-provider.test.ts │ │ ├── executor.test.ts │ │ └── smoke.test.ts │ ├── tsconfig.json │ └── vitest.config.ts ├── README-task-master.md ├── README.md ├── scripts │ ├── dev.js │ ├── init.js │ ├── modules │ │ ├── ai-services-unified.js │ │ ├── commands.js │ │ ├── config-manager.js │ │ ├── dependency-manager.js │ │ ├── index.js │ │ ├── prompt-manager.js │ │ ├── supported-models.json │ │ ├── sync-readme.js │ │ ├── task-manager │ │ │ ├── add-subtask.js │ │ │ ├── add-task.js │ │ │ ├── analyze-task-complexity.js │ │ │ ├── clear-subtasks.js │ │ │ ├── expand-all-tasks.js │ │ │ ├── expand-task.js │ │ │ ├── find-next-task.js │ │ │ ├── generate-task-files.js │ │ │ ├── is-task-dependent.js │ │ │ ├── list-tasks.js │ │ │ ├── migrate.js │ │ │ ├── models.js │ │ │ ├── move-task.js │ │ │ ├── parse-prd │ │ │ │ ├── index.js │ │ │ │ ├── parse-prd-config.js │ │ │ │ ├── parse-prd-helpers.js │ │ │ │ ├── parse-prd-non-streaming.js │ │ │ │ ├── parse-prd-streaming.js │ │ │ │ └── parse-prd.js │ │ │ ├── remove-subtask.js │ │ │ ├── remove-task.js │ │ │ ├── research.js │ │ │ ├── response-language.js │ │ │ ├── scope-adjustment.js │ │ │ ├── set-task-status.js │ │ │ ├── tag-management.js │ │ │ ├── task-exists.js │ │ │ ├── update-single-task-status.js │ │ │ ├── update-subtask-by-id.js │ │ │ ├── update-task-by-id.js │ │ │ └── update-tasks.js │ │ ├── task-manager.js │ │ ├── ui.js │ │ ├── update-config-tokens.js │ │ ├── utils │ │ │ ├── contextGatherer.js │ │ │ ├── fuzzyTaskSearch.js │ │ │ └── git-utils.js │ │ └── utils.js │ ├── task-complexity-report.json │ ├── test-claude-errors.js │ └── test-claude.js ├── src │ ├── ai-providers │ │ ├── anthropic.js │ │ ├── azure.js │ │ ├── base-provider.js │ │ ├── bedrock.js │ │ ├── claude-code.js │ │ ├── custom-sdk │ │ │ ├── claude-code │ │ │ │ ├── errors.js │ │ │ │ ├── index.js │ │ │ │ ├── json-extractor.js │ │ │ │ ├── language-model.js │ │ │ │ ├── message-converter.js │ │ │ │ └── types.js │ │ │ └── grok-cli │ │ │ ├── errors.js │ │ │ ├── index.js │ │ │ ├── json-extractor.js │ │ │ ├── language-model.js │ │ │ ├── message-converter.js │ │ │ └── types.js │ │ ├── gemini-cli.js │ │ ├── google-vertex.js │ │ ├── google.js │ │ ├── grok-cli.js │ │ ├── groq.js │ │ ├── index.js │ │ ├── ollama.js │ │ ├── openai.js │ │ ├── openrouter.js │ │ ├── perplexity.js │ │ └── xai.js │ ├── constants │ │ ├── commands.js │ │ ├── paths.js │ │ ├── profiles.js │ │ ├── providers.js │ │ ├── rules-actions.js │ │ ├── task-priority.js │ │ └── task-status.js │ ├── profiles │ │ ├── amp.js │ │ ├── base-profile.js │ │ ├── claude.js │ │ ├── cline.js │ │ ├── codex.js │ │ ├── cursor.js │ │ ├── gemini.js │ │ ├── index.js │ │ ├── kilo.js │ │ ├── kiro.js │ │ ├── opencode.js │ │ ├── roo.js │ │ ├── trae.js │ │ ├── vscode.js │ │ ├── windsurf.js │ │ └── zed.js │ ├── progress │ │ ├── base-progress-tracker.js │ │ ├── cli-progress-factory.js │ │ ├── parse-prd-tracker.js │ │ ├── progress-tracker-builder.js │ │ └── tracker-ui.js │ ├── prompts │ │ ├── add-task.json │ │ ├── analyze-complexity.json │ │ ├── expand-task.json │ │ ├── parse-prd.json │ │ ├── README.md │ │ ├── research.json │ │ ├── schemas │ │ │ ├── parameter.schema.json │ │ │ ├── prompt-template.schema.json │ │ │ ├── README.md │ │ │ └── variant.schema.json │ │ ├── update-subtask.json │ │ ├── update-task.json │ │ └── update-tasks.json │ ├── provider-registry │ │ └── index.js │ ├── task-master.js │ ├── ui │ │ ├── confirm.js │ │ ├── indicators.js │ │ └── parse-prd.js │ └── utils │ ├── asset-resolver.js │ ├── create-mcp-config.js │ ├── format.js │ ├── getVersion.js │ ├── logger-utils.js │ ├── manage-gitignore.js │ ├── path-utils.js │ ├── profiles.js │ ├── rule-transformer.js │ ├── stream-parser.js │ └── timeout-manager.js ├── test-clean-tags.js ├── test-config-manager.js ├── test-prd.txt ├── test-tag-functions.js ├── test-version-check-full.js ├── test-version-check.js ├── tests │ ├── e2e │ │ ├── e2e_helpers.sh │ │ ├── parse_llm_output.cjs │ │ ├── run_e2e.sh │ │ ├── run_fallback_verification.sh │ │ └── test_llm_analysis.sh │ ├── fixture │ │ └── test-tasks.json │ ├── fixtures │ │ ├── .taskmasterconfig │ │ ├── sample-claude-response.js │ │ ├── sample-prd.txt │ │ └── sample-tasks.js │ ├── integration │ │ ├── claude-code-optional.test.js │ │ ├── cli │ │ │ ├── commands.test.js │ │ │ ├── complex-cross-tag-scenarios.test.js │ │ │ └── move-cross-tag.test.js │ │ ├── manage-gitignore.test.js │ │ ├── mcp-server │ │ │ └── direct-functions.test.js │ │ ├── move-task-cross-tag.integration.test.js │ │ ├── move-task-simple.integration.test.js │ │ └── profiles │ │ ├── amp-init-functionality.test.js │ │ ├── claude-init-functionality.test.js │ │ ├── cline-init-functionality.test.js │ │ ├── codex-init-functionality.test.js │ │ ├── cursor-init-functionality.test.js │ │ ├── gemini-init-functionality.test.js │ │ ├── opencode-init-functionality.test.js │ │ ├── roo-files-inclusion.test.js │ │ ├── roo-init-functionality.test.js │ │ ├── rules-files-inclusion.test.js │ │ ├── trae-init-functionality.test.js │ │ ├── vscode-init-functionality.test.js │ │ └── windsurf-init-functionality.test.js │ ├── manual │ │ ├── progress │ │ │ ├── parse-prd-analysis.js │ │ │ ├── test-parse-prd.js │ │ │ └── TESTING_GUIDE.md │ │ └── prompts │ │ ├── prompt-test.js │ │ └── README.md │ ├── README.md │ ├── setup.js │ └── unit │ ├── ai-providers │ │ ├── claude-code.test.js │ │ ├── custom-sdk │ │ │ └── claude-code │ │ │ └── language-model.test.js │ │ ├── gemini-cli.test.js │ │ ├── mcp-components.test.js │ │ └── openai.test.js │ ├── ai-services-unified.test.js │ ├── commands.test.js │ ├── config-manager.test.js │ ├── config-manager.test.mjs │ ├── dependency-manager.test.js │ ├── init.test.js │ ├── initialize-project.test.js │ ├── kebab-case-validation.test.js │ ├── manage-gitignore.test.js │ ├── mcp │ │ └── tools │ │ ├── __mocks__ │ │ │ └── move-task.js │ │ ├── add-task.test.js │ │ ├── analyze-complexity.test.js │ │ ├── expand-all.test.js │ │ ├── get-tasks.test.js │ │ ├── initialize-project.test.js │ │ ├── move-task-cross-tag-options.test.js │ │ ├── move-task-cross-tag.test.js │ │ └── remove-task.test.js │ ├── mcp-providers │ │ ├── mcp-components.test.js │ │ └── mcp-provider.test.js │ ├── parse-prd.test.js │ ├── profiles │ │ ├── amp-integration.test.js │ │ ├── claude-integration.test.js │ │ ├── cline-integration.test.js │ │ ├── codex-integration.test.js │ │ ├── cursor-integration.test.js │ │ ├── gemini-integration.test.js │ │ ├── kilo-integration.test.js │ │ ├── kiro-integration.test.js │ │ ├── mcp-config-validation.test.js │ │ ├── opencode-integration.test.js │ │ ├── profile-safety-check.test.js │ │ ├── roo-integration.test.js │ │ ├── rule-transformer-cline.test.js │ │ ├── rule-transformer-cursor.test.js │ │ ├── rule-transformer-gemini.test.js │ │ ├── rule-transformer-kilo.test.js │ │ ├── rule-transformer-kiro.test.js │ │ ├── rule-transformer-opencode.test.js │ │ ├── rule-transformer-roo.test.js │ │ ├── rule-transformer-trae.test.js │ │ ├── rule-transformer-vscode.test.js │ │ ├── rule-transformer-windsurf.test.js │ │ ├── rule-transformer-zed.test.js │ │ ├── rule-transformer.test.js │ │ ├── selective-profile-removal.test.js │ │ ├── subdirectory-support.test.js │ │ ├── trae-integration.test.js │ │ ├── vscode-integration.test.js │ │ ├── windsurf-integration.test.js │ │ └── zed-integration.test.js │ ├── progress │ │ └── base-progress-tracker.test.js │ ├── prompt-manager.test.js │ ├── prompts │ │ └── expand-task-prompt.test.js │ ├── providers │ │ └── provider-registry.test.js │ ├── scripts │ │ └── modules │ │ ├── commands │ │ │ ├── move-cross-tag.test.js │ │ │ └── README.md │ │ ├── dependency-manager │ │ │ ├── circular-dependencies.test.js │ │ │ ├── cross-tag-dependencies.test.js │ │ │ └── fix-dependencies-command.test.js │ │ ├── task-manager │ │ │ ├── add-subtask.test.js │ │ │ ├── add-task.test.js │ │ │ ├── analyze-task-complexity.test.js │ │ │ ├── clear-subtasks.test.js │ │ │ ├── complexity-report-tag-isolation.test.js │ │ │ ├── expand-all-tasks.test.js │ │ │ ├── expand-task.test.js │ │ │ ├── find-next-task.test.js │ │ │ ├── generate-task-files.test.js │ │ │ ├── list-tasks.test.js │ │ │ ├── move-task-cross-tag.test.js │ │ │ ├── move-task.test.js │ │ │ ├── parse-prd.test.js │ │ │ ├── remove-subtask.test.js │ │ │ ├── remove-task.test.js │ │ │ ├── research.test.js │ │ │ ├── scope-adjustment.test.js │ │ │ ├── set-task-status.test.js │ │ │ ├── setup.js │ │ │ ├── update-single-task-status.test.js │ │ │ ├── update-subtask-by-id.test.js │ │ │ ├── update-task-by-id.test.js │ │ │ └── update-tasks.test.js │ │ ├── ui │ │ │ └── cross-tag-error-display.test.js │ │ └── utils-tag-aware-paths.test.js │ ├── task-finder.test.js │ ├── task-manager │ │ ├── clear-subtasks.test.js │ │ ├── move-task.test.js │ │ ├── tag-boundary.test.js │ │ └── tag-management.test.js │ ├── task-master.test.js │ ├── ui │ │ └── indicators.test.js │ ├── ui.test.js │ ├── utils-strip-ansi.test.js │ └── utils.test.js ├── tsconfig.json ├── tsdown.config.ts └── turbo.json ``` # Files -------------------------------------------------------------------------------- /src/progress/base-progress-tracker.js: -------------------------------------------------------------------------------- ```javascript import { newMultiBar } from './cli-progress-factory.js'; /** * Base class for progress trackers, handling common logic for time, tokens, estimation, and multibar management. */ export class BaseProgressTracker { constructor(options = {}) { this.numUnits = options.numUnits || 1; this.unitName = options.unitName || 'unit'; // e.g., 'task', 'subtask' this.startTime = null; this.completedUnits = 0; this.tokensIn = 0; this.tokensOut = 0; this.isEstimate = true; // For token display // Time estimation properties this.bestAvgTimePerUnit = null; this.lastEstimateTime = null; this.lastEstimateSeconds = 0; // UI components this.multibar = null; this.timeTokensBar = null; this.progressBar = null; this._timerInterval = null; // State flags this.isStarted = false; this.isFinished = false; // Allow subclasses to define custom properties this._initializeCustomProperties(options); } /** * Protected method for subclasses to initialize custom properties. * @protected */ _initializeCustomProperties(options) { // Subclasses can override this } /** * Get the pluralized form of the unit name for safe property keys. * @returns {string} Pluralized unit name */ get unitNamePlural() { return `${this.unitName}s`; } start() { if (this.isStarted || this.isFinished) return; this.isStarted = true; this.startTime = Date.now(); this.multibar = newMultiBar(); // Create time/tokens bar using subclass-provided format this.timeTokensBar = this.multibar.create( 1, 0, {}, { format: this._getTimeTokensBarFormat(), barsize: 1, hideCursor: true, clearOnComplete: false } ); // Create main progress bar using subclass-provided format this.progressBar = this.multibar.create( this.numUnits, 0, {}, { format: this._getProgressBarFormat(), barCompleteChar: '\u2588', barIncompleteChar: '\u2591' } ); this._updateTimeTokensBar(); this.progressBar.update(0, { [this.unitNamePlural]: `0/${this.numUnits}` }); // Start timer this._timerInterval = setInterval(() => this._updateTimeTokensBar(), 1000); // Allow subclasses to add custom bars or setup this._setupCustomUI(); } /** * Protected method for subclasses to add custom UI elements after start. * @protected */ _setupCustomUI() { // Subclasses can override this } /** * Protected method to get the format for the time/tokens bar. * @protected * @returns {string} Format string for the time/tokens bar. */ _getTimeTokensBarFormat() { return `{clock} {elapsed} | Tokens (I/O): {in}/{out} | Est: {remaining}`; } /** * Protected method to get the format for the main progress bar. * @protected * @returns {string} Format string for the progress bar. */ _getProgressBarFormat() { return `${this.unitName.charAt(0).toUpperCase() + this.unitName.slice(1)}s {${this.unitNamePlural}} |{bar}| {percentage}%`; } updateTokens(tokensIn, tokensOut, isEstimate = false) { this.tokensIn = tokensIn || 0; this.tokensOut = tokensOut || 0; this.isEstimate = isEstimate; this._updateTimeTokensBar(); } _updateTimeTokensBar() { if (!this.timeTokensBar || this.isFinished) return; const elapsed = this._formatElapsedTime(); const remaining = this._estimateRemainingTime(); const tokensLabel = this.isEstimate ? '~ Tokens (I/O)' : 'Tokens (I/O)'; this.timeTokensBar.update(1, { clock: '⏱️', elapsed, in: this.tokensIn, out: this.tokensOut, remaining, tokensLabel, // Subclasses can add more payload here via override ...this._getCustomTimeTokensPayload() }); } /** * Protected method for subclasses to provide custom payload for time/tokens bar. * @protected * @returns {Object} Custom payload object. */ _getCustomTimeTokensPayload() { return {}; } _formatElapsedTime() { if (!this.startTime) return '0m 00s'; const seconds = Math.floor((Date.now() - this.startTime) / 1000); const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; return `${minutes}m ${remainingSeconds.toString().padStart(2, '0')}s`; } _estimateRemainingTime() { const progress = this._getProgressFraction(); if (progress >= 1) return '~0s'; const now = Date.now(); const elapsed = (now - this.startTime) / 1000; if (progress === 0) return '~calculating...'; const avgTimePerUnit = elapsed / progress; if ( this.bestAvgTimePerUnit === null || avgTimePerUnit < this.bestAvgTimePerUnit ) { this.bestAvgTimePerUnit = avgTimePerUnit; } const remainingUnits = this.numUnits * (1 - progress); let estimatedSeconds = Math.ceil(remainingUnits * this.bestAvgTimePerUnit); // Stabilization logic if (this.lastEstimateTime) { const elapsedSinceEstimate = Math.floor( (now - this.lastEstimateTime) / 1000 ); const countdownSeconds = Math.max( 0, this.lastEstimateSeconds - elapsedSinceEstimate ); if (countdownSeconds === 0) return '~0s'; estimatedSeconds = Math.min(estimatedSeconds, countdownSeconds); } this.lastEstimateTime = now; this.lastEstimateSeconds = estimatedSeconds; return `~${this._formatDuration(estimatedSeconds)}`; } /** * Protected method for subclasses to calculate current progress fraction (0-1). * Defaults to simple completedUnits / numUnits. * @protected * @returns {number} Progress fraction (can be fractional for subtasks). */ _getProgressFraction() { return this.completedUnits / this.numUnits; } _formatDuration(seconds) { if (seconds < 60) return `${seconds}s`; const minutes = Math.floor(seconds / 60); const remainingSeconds = seconds % 60; if (minutes < 60) { return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`; } const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; return `${hours}h ${remainingMinutes}m`; } getElapsedTime() { return this.startTime ? Date.now() - this.startTime : 0; } stop() { if (this.isFinished) return; this.isFinished = true; if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } if (this.multibar) { this._updateTimeTokensBar(); this.multibar.stop(); } // Ensure cleanup is called to prevent memory leaks this.cleanup(); } getSummary() { return { completedUnits: this.completedUnits, elapsedTime: this.getElapsedTime() // Subclasses should extend this }; } /** * Cleanup method to ensure proper resource disposal and prevent memory leaks. * Should be called when the progress tracker is no longer needed. */ cleanup() { // Stop any active timers if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } // Stop and clear multibar if (this.multibar) { try { this.multibar.stop(); } catch (error) { // Ignore errors during cleanup } this.multibar = null; } // Clear progress bar references this.timeTokensBar = null; this.progressBar = null; // Reset state this.isStarted = false; this.isFinished = true; // Allow subclasses to perform custom cleanup this._performCustomCleanup(); } /** * Protected method for subclasses to perform custom cleanup. * @protected */ _performCustomCleanup() { // Subclasses can override this } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/direct-functions/expand-task.js: -------------------------------------------------------------------------------- ```javascript /** * expand-task.js * Direct function implementation for expanding a task into subtasks */ import expandTask from '../../../../scripts/modules/task-manager/expand-task.js'; import { readJSON, writeJSON, enableSilentMode, disableSilentMode, isSilentMode } from '../../../../scripts/modules/utils.js'; import path from 'path'; import fs from 'fs'; import { createLogWrapper } from '../../tools/utils.js'; /** * Direct function wrapper for expanding a task into subtasks with error handling. * * @param {Object} args - Command arguments * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. * @param {string} args.id - The ID of the task to expand. * @param {number|string} [args.num] - Number of subtasks to generate. * @param {boolean} [args.research] - Enable research role for subtask generation. * @param {string} [args.prompt] - Additional context to guide subtask generation. * @param {boolean} [args.force] - Force expansion even if subtasks exist. * @param {string} [args.projectRoot] - Project root directory. * @param {string} [args.tag] - Tag for the task * @param {Object} log - Logger object * @param {Object} context - Context object containing session * @param {Object} [context.session] - MCP Session object * @returns {Promise<Object>} - Task expansion result { success: boolean, data?: any, error?: { code: string, message: string } } */ export async function expandTaskDirect(args, log, context = {}) { const { session } = context; // Extract session // Destructure expected args, including projectRoot const { tasksJsonPath, id, num, research, prompt, force, projectRoot, tag, complexityReportPath } = args; // Log session root data for debugging log.info( `Session data in expandTaskDirect: ${JSON.stringify({ hasSession: !!session, sessionKeys: session ? Object.keys(session) : [], roots: session?.roots, rootsStr: JSON.stringify(session?.roots) })}` ); // Check if tasksJsonPath was provided if (!tasksJsonPath) { log.error('expandTaskDirect called without tasksJsonPath'); return { success: false, error: { code: 'MISSING_ARGUMENT', message: 'tasksJsonPath is required' } }; } // Use provided path const tasksPath = tasksJsonPath; log.info(`[expandTaskDirect] Using tasksPath: ${tasksPath}`); // Validate task ID const taskId = id ? parseInt(id, 10) : null; if (!taskId) { log.error('Task ID is required'); return { success: false, error: { code: 'INPUT_VALIDATION_ERROR', message: 'Task ID is required' } }; } // Process other parameters const numSubtasks = num ? parseInt(num, 10) : undefined; const useResearch = research === true; const additionalContext = prompt || ''; const forceFlag = force === true; try { log.info( `[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}, Force: ${forceFlag}` ); // Read tasks data log.info(`[expandTaskDirect] Attempting to read JSON from: ${tasksPath}`); const data = readJSON(tasksPath, projectRoot); log.info( `[expandTaskDirect] Result of readJSON: ${data ? 'Data read successfully' : 'readJSON returned null or undefined'}` ); if (!data || !data.tasks) { log.error( `[expandTaskDirect] readJSON failed or returned invalid data for path: ${tasksPath}` ); return { success: false, error: { code: 'INVALID_TASKS_FILE', message: `No valid tasks found in ${tasksPath}. readJSON returned: ${JSON.stringify(data)}` } }; } // Find the specific task log.info(`[expandTaskDirect] Searching for task ID ${taskId} in data`); const task = data.tasks.find((t) => t.id === taskId); log.info(`[expandTaskDirect] Task found: ${task ? 'Yes' : 'No'}`); if (!task) { return { success: false, error: { code: 'TASK_NOT_FOUND', message: `Task with ID ${taskId} not found` } }; } // Check if task is completed if (task.status === 'done' || task.status === 'completed') { return { success: false, error: { code: 'TASK_COMPLETED', message: `Task ${taskId} is already marked as ${task.status} and cannot be expanded` } }; } // Check for existing subtasks and force flag const hasExistingSubtasks = task.subtasks && task.subtasks.length > 0; if (hasExistingSubtasks && !forceFlag) { log.info( `Task ${taskId} already has ${task.subtasks.length} subtasks. Use --force to overwrite.` ); return { success: true, data: { message: `Task ${taskId} already has subtasks. Expansion skipped.`, task, subtasksAdded: 0, hasExistingSubtasks } }; } // If force flag is set, clear existing subtasks if (hasExistingSubtasks && forceFlag) { log.info( `Force flag set. Clearing existing subtasks for task ${taskId}.` ); task.subtasks = []; } // Keep a copy of the task before modification const originalTask = JSON.parse(JSON.stringify(task)); // Tracking subtasks count before expansion const subtasksCountBefore = task.subtasks ? task.subtasks.length : 0; // Directly modify the data instead of calling the CLI function if (!task.subtasks) { task.subtasks = []; } // Save tasks.json with potentially empty subtasks array and proper context writeJSON(tasksPath, data, projectRoot, tag); // Create logger wrapper using the utility const mcpLog = createLogWrapper(log); let wasSilent; // Declare wasSilent outside the try block // Process the request try { // Enable silent mode to prevent console logs from interfering with JSON response wasSilent = isSilentMode(); // Assign inside the try block if (!wasSilent) enableSilentMode(); // Call the core expandTask function with the wrapped logger and projectRoot const coreResult = await expandTask( tasksPath, taskId, numSubtasks, useResearch, additionalContext, { complexityReportPath, mcpLog, session, projectRoot, commandName: 'expand-task', outputType: 'mcp', tag }, forceFlag ); // Restore normal logging if (!wasSilent && isSilentMode()) disableSilentMode(); // Read the updated data const updatedData = readJSON(tasksPath, projectRoot); const updatedTask = updatedData.tasks.find((t) => t.id === taskId); // Calculate how many subtasks were added const subtasksAdded = updatedTask.subtasks ? updatedTask.subtasks.length - subtasksCountBefore : 0; // Return the result, including telemetryData log.info( `Successfully expanded task ${taskId} with ${subtasksAdded} new subtasks` ); return { success: true, data: { task: coreResult.task, subtasksAdded, hasExistingSubtasks, telemetryData: coreResult.telemetryData, tagInfo: coreResult.tagInfo } }; } catch (error) { // Make sure to restore normal logging even if there's an error if (!wasSilent && isSilentMode()) disableSilentMode(); log.error(`Error expanding task: ${error.message}`); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: error.message || 'Failed to expand task' } }; } } catch (error) { log.error(`Error expanding task: ${error.message}`); return { success: false, error: { code: 'CORE_FUNCTION_ERROR', message: error.message || 'Failed to expand task' } }; } } ``` -------------------------------------------------------------------------------- /apps/extension/src/webview/index.css: -------------------------------------------------------------------------------- ```css @import "tailwindcss"; /* shadcn/ui CSS variables */ @theme { /* VS Code CSS variables will be injected here */ /* color-scheme: var(--vscode-theme-kind, light); */ /* shadcn/ui variables - adapted for VS Code */ --color-background: var(--vscode-editor-background); --color-sidebar-background: var(--vscode-sideBar-background); --color-foreground: var(--vscode-foreground); --color-card: var(--vscode-editor-background); --color-card-foreground: var(--vscode-foreground); --color-popover: var(--vscode-editor-background); --color-popover-foreground: var(--vscode-foreground); --color-primary: var(--vscode-button-background); --color-primary-foreground: var(--vscode-button-foreground); --color-secondary: var(--vscode-button-secondaryBackground); --color-secondary-foreground: var(--vscode-button-secondaryForeground); --color-widget-background: var(--vscode-editorWidget-background); --color-widget-border: var(--vscode-editorWidget-border); --color-code-snippet-background: var(--vscode-textPreformat-background); --color-code-snippet-text: var(--vscode-textPreformat-foreground); --font-editor-font: var(--vscode-editor-font-family); --font-editor-size: var(--vscode-editor-font-size); --color-input-background: var(--vscode-input-background); --color-input-foreground: var(--vscode-input-foreground); --color-accent: var(--vscode-focusBorder); --color-accent-foreground: var(--vscode-foreground); --color-destructive: var(--vscode-errorForeground); --color-destructive-foreground: var(--vscode-foreground); --color-border: var(--vscode-panel-border); --color-ring: var(--vscode-focusBorder); --color-link: var(--vscode-editorLink-foreground); --color-link-hover: var(--vscode-editorLink-activeForeground); --color-textSeparator-foreground: var(--vscode-textSeparator-foreground); --radius: 0.5rem; /* VS Code specific color mappings for Tailwind utilities */ --color-vscode-foreground: var(--vscode-foreground); --color-vscode-button-background: var(--vscode-button-background); --color-vscode-button-foreground: var(--vscode-button-foreground); --color-vscode-button-hoverBackground: var(--vscode-button-hoverBackground); --color-vscode-editor-background: var(--vscode-editor-background); --color-vscode-input-background: var(--vscode-input-background); --color-vscode-input-foreground: var(--vscode-input-foreground); --color-vscode-dropdown-background: var(--vscode-dropdown-background); --color-vscode-dropdown-foreground: var(--vscode-dropdown-foreground); --color-vscode-dropdown-border: var(--vscode-dropdown-border); --color-vscode-focusBorder: var(--vscode-focusBorder); --color-vscode-panel-border: var(--vscode-panel-border); --color-vscode-sideBar-background: var(--vscode-sideBar-background); --color-vscode-sideBar-foreground: var(--vscode-sideBar-foreground); --color-vscode-sideBarTitle-foreground: var(--vscode-sideBarTitle-foreground); --color-vscode-testing-iconPassed: var(--vscode-testing-iconPassed); --color-vscode-testing-iconFailed: var(--vscode-testing-iconFailed); --color-vscode-errorForeground: var(--vscode-errorForeground); --color-vscode-editorWidget-background: var(--vscode-editorWidget-background); --color-vscode-editorWidget-border: var(--vscode-editorWidget-border); --color-vscode-list-hoverBackground: var(--vscode-list-hoverBackground); --color-vscode-list-activeSelectionBackground: var( --vscode-list-activeSelectionBackground ); --color-vscode-list-activeSelectionForeground: var( --vscode-list-activeSelectionForeground ); --color-vscode-badge-background: var(--vscode-badge-background); --color-vscode-badge-foreground: var(--vscode-badge-foreground); --color-vscode-textLink-foreground: var(--vscode-textLink-foreground); --color-vscode-textLink-activeForeground: var( --vscode-textLink-activeForeground ); --color-vscode-icon-foreground: var(--vscode-icon-foreground); --color-vscode-descriptionForeground: var(--vscode-descriptionForeground); --color-vscode-disabledForeground: var(--vscode-disabledForeground); } /* Reset body to match VS Code styles instead of Tailwind defaults */ @layer base { html, body { height: 100%; margin: 0 !important; padding: 0 !important; overflow: hidden; } body { background-color: var(--vscode-editor-background) !important; color: var(--vscode-foreground) !important; font-family: var(--vscode-font-family) !important; font-size: var(--vscode-font-size) !important; font-weight: var(--vscode-font-weight) !important; line-height: 1.4 !important; } /* Ensure root container takes full space */ #root { height: 100vh; width: 100vw; display: flex; flex-direction: column; overflow: hidden; } /* Override any conflicting Tailwind defaults for VS Code integration */ * { box-sizing: border-box; } /* Ensure buttons and inputs use VS Code styling */ button, input, select, textarea { font-family: inherit; } } /* Enhanced scrollbar styling for Kanban board */ ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-track { background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1)); border-radius: 4px; } ::-webkit-scrollbar-thumb { background: var( --vscode-scrollbarSlider-hoverBackground, rgba(255, 255, 255, 0.2) ); border-radius: 4px; border: 1px solid transparent; background-clip: padding-box; } ::-webkit-scrollbar-thumb:hover { background: var( --vscode-scrollbarSlider-activeBackground, rgba(255, 255, 255, 0.3) ); } ::-webkit-scrollbar-corner { background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1)); } /* Kanban specific styles */ @layer components { .kanban-container { scrollbar-gutter: stable; } /* Smooth scrolling for better UX */ .kanban-container { scroll-behavior: smooth; } /* Ensure proper touch scrolling on mobile */ .kanban-container { -webkit-overflow-scrolling: touch; } /* Add subtle shadow for depth */ .kanban-column { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } /* Enhanced scrolling for column content areas */ .kanban-column > div[style*="overflow-y"] { scrollbar-width: thin; scrollbar-color: var( --vscode-scrollbarSlider-hoverBackground, rgba(255, 255, 255, 0.2) ) var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1)); } /* Card hover effects */ .kanban-card { transition: all 0.2s ease-in-out; } .kanban-card:hover { transform: translateY(-1px); } /* Focus indicators for accessibility */ .kanban-card:focus-visible { outline: 2px solid var(--vscode-focusBorder); outline-offset: 2px; } } /* Line clamp utility for text truncation */ @layer utilities { .line-clamp-2 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; } .line-clamp-3 { overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; } /* Custom scrollbar utilities */ .scrollbar-thin { scrollbar-width: thin; } .scrollbar-track-transparent { scrollbar-color: var( --vscode-scrollbarSlider-hoverBackground, rgba(255, 255, 255, 0.2) ) transparent; } } /* Dark mode adjustments */ @media (prefers-color-scheme: dark) { ::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); } ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.15); } ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.25); } } ``` -------------------------------------------------------------------------------- /src/ui/indicators.js: -------------------------------------------------------------------------------- ```javascript /** * indicators.js * UI functions for displaying priority and complexity indicators in different contexts */ import chalk from 'chalk'; import { TASK_PRIORITY_OPTIONS } from '../constants/task-priority.js'; // Extract priority values for cleaner object keys const [HIGH, MEDIUM, LOW] = TASK_PRIORITY_OPTIONS; // Cache for generated indicators const INDICATOR_CACHE = new Map(); /** * Base configuration for indicator systems */ class IndicatorConfig { constructor(name, levels, colors, thresholds = null) { this.name = name; this.levels = levels; this.colors = colors; this.thresholds = thresholds; } getColor(level) { return this.colors[level] || chalk.gray; } getLevelFromScore(score) { if (!this.thresholds) { throw new Error(`${this.name} does not support score-based levels`); } if (score >= 7) return this.levels[0]; // high if (score <= 3) return this.levels[2]; // low return this.levels[1]; // medium } } /** * Visual style definitions */ const VISUAL_STYLES = { cli: { filled: '●', // ● empty: '○' // ○ }, statusBar: { high: '⋮', // ⋮ medium: ':', // : low: '.' // . }, mcp: { high: '🔴', // 🔴 medium: '🟠', // 🟠 low: '🟢' // 🟢 } }; /** * Priority configuration */ const PRIORITY_CONFIG = new IndicatorConfig('priority', [HIGH, MEDIUM, LOW], { [HIGH]: chalk.hex('#CC0000'), [MEDIUM]: chalk.hex('#FF8800'), [LOW]: chalk.yellow }); /** * Generates CLI indicator with intensity */ function generateCliIndicator(intensity, color) { const filled = VISUAL_STYLES.cli.filled; const empty = VISUAL_STYLES.cli.empty; let indicator = ''; for (let i = 0; i < 3; i++) { if (i < intensity) { indicator += color(filled); } else { indicator += chalk.white(empty); } } return indicator; } /** * Get intensity level from priority/complexity level */ function getIntensityFromLevel(level, levels) { const index = levels.indexOf(level); return 3 - index; // high=3, medium=2, low=1 } /** * Generic cached indicator getter * @param {string} cacheKey - Cache key for the indicators * @param {Function} generator - Function to generate the indicators * @returns {Object} Cached or newly generated indicators */ function getCachedIndicators(cacheKey, generator) { if (INDICATOR_CACHE.has(cacheKey)) { return INDICATOR_CACHE.get(cacheKey); } const indicators = generator(); INDICATOR_CACHE.set(cacheKey, indicators); return indicators; } /** * Get priority indicators for MCP context (single emojis) * @returns {Object} Priority to emoji mapping */ export function getMcpPriorityIndicators() { return getCachedIndicators('mcp-priority-all', () => ({ [HIGH]: VISUAL_STYLES.mcp.high, [MEDIUM]: VISUAL_STYLES.mcp.medium, [LOW]: VISUAL_STYLES.mcp.low })); } /** * Get priority indicators for CLI context (colored dots with visual hierarchy) * @returns {Object} Priority to colored dot string mapping */ export function getCliPriorityIndicators() { return getCachedIndicators('cli-priority-all', () => { const indicators = {}; PRIORITY_CONFIG.levels.forEach((level) => { const intensity = getIntensityFromLevel(level, PRIORITY_CONFIG.levels); const color = PRIORITY_CONFIG.getColor(level); indicators[level] = generateCliIndicator(intensity, color); }); return indicators; }); } /** * Get priority indicators for status bars (simplified single character versions) * @returns {Object} Priority to single character indicator mapping */ export function getStatusBarPriorityIndicators() { return getCachedIndicators('statusbar-priority-all', () => { const indicators = {}; PRIORITY_CONFIG.levels.forEach((level, index) => { const style = index === 0 ? VISUAL_STYLES.statusBar.high : index === 1 ? VISUAL_STYLES.statusBar.medium : VISUAL_STYLES.statusBar.low; const color = PRIORITY_CONFIG.getColor(level); indicators[level] = color(style); }); return indicators; }); } /** * Get priority colors for consistent styling * @returns {Object} Priority to chalk color function mapping */ export function getPriorityColors() { return { [HIGH]: PRIORITY_CONFIG.colors[HIGH], [MEDIUM]: PRIORITY_CONFIG.colors[MEDIUM], [LOW]: PRIORITY_CONFIG.colors[LOW] }; } /** * Get priority indicators based on context * @param {boolean} isMcp - Whether this is for MCP context (true) or CLI context (false) * @returns {Object} Priority to indicator mapping */ export function getPriorityIndicators(isMcp = false) { return isMcp ? getMcpPriorityIndicators() : getCliPriorityIndicators(); } /** * Get a specific priority indicator * @param {string} priority - The priority level ('high', 'medium', 'low') * @param {boolean} isMcp - Whether this is for MCP context * @returns {string} The indicator string for the priority */ export function getPriorityIndicator(priority, isMcp = false) { const indicators = getPriorityIndicators(isMcp); return indicators[priority] || indicators[MEDIUM]; } // ============================================================================ // Complexity Indicators // ============================================================================ /** * Complexity configuration */ const COMPLEXITY_CONFIG = new IndicatorConfig( 'complexity', ['high', 'medium', 'low'], { high: chalk.hex('#CC0000'), medium: chalk.hex('#FF8800'), low: chalk.green }, { high: (score) => score >= 7, medium: (score) => score >= 4 && score <= 6, low: (score) => score <= 3 } ); /** * Get complexity indicators for CLI context (colored dots with visual hierarchy) * Complexity scores: 1-3 (low), 4-6 (medium), 7-10 (high) * @returns {Object} Complexity level to colored dot string mapping */ export function getCliComplexityIndicators() { return getCachedIndicators('cli-complexity-all', () => { const indicators = {}; COMPLEXITY_CONFIG.levels.forEach((level) => { const intensity = getIntensityFromLevel(level, COMPLEXITY_CONFIG.levels); const color = COMPLEXITY_CONFIG.getColor(level); indicators[level] = generateCliIndicator(intensity, color); }); return indicators; }); } /** * Get complexity indicators for status bars (simplified single character versions) * @returns {Object} Complexity level to single character indicator mapping */ export function getStatusBarComplexityIndicators() { return getCachedIndicators('statusbar-complexity-all', () => { const indicators = {}; COMPLEXITY_CONFIG.levels.forEach((level, index) => { const style = index === 0 ? VISUAL_STYLES.statusBar.high : index === 1 ? VISUAL_STYLES.statusBar.medium : VISUAL_STYLES.statusBar.low; const color = COMPLEXITY_CONFIG.getColor(level); indicators[level] = color(style); }); return indicators; }); } /** * Get complexity colors for consistent styling * @returns {Object} Complexity level to chalk color function mapping */ export function getComplexityColors() { return { ...COMPLEXITY_CONFIG.colors }; } /** * Get a specific complexity indicator based on score * @param {number} score - The complexity score (1-10) * @param {boolean} statusBar - Whether to return status bar version (single char) * @returns {string} The indicator string for the complexity level */ export function getComplexityIndicator(score, statusBar = false) { const level = COMPLEXITY_CONFIG.getLevelFromScore(score); const indicators = statusBar ? getStatusBarComplexityIndicators() : getCliComplexityIndicators(); return indicators[level]; } ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/expand-all-tasks.js: -------------------------------------------------------------------------------- ```javascript import { log, readJSON, isSilentMode, findProjectRoot } from '../utils.js'; import { startLoadingIndicator, stopLoadingIndicator, displayAiUsageSummary } from '../ui.js'; import expandTask from './expand-task.js'; import { getDebugFlag } from '../config-manager.js'; import { aggregateTelemetry } from '../utils.js'; import chalk from 'chalk'; import boxen from 'boxen'; /** * Expand all eligible pending or in-progress tasks using the expandTask function. * @param {string} tasksPath - Path to the tasks.json file * @param {number} [numSubtasks] - Optional: Target number of subtasks per task. * @param {boolean} [useResearch=false] - Whether to use the research AI role. * @param {string} [additionalContext=''] - Optional additional context. * @param {boolean} [force=false] - Force expansion even if tasks already have subtasks. * @param {Object} context - Context object containing session and mcpLog. * @param {Object} [context.session] - Session object from MCP. * @param {Object} [context.mcpLog] - MCP logger object. * @param {string} [context.projectRoot] - Project root path * @param {string} [context.tag] - Tag for the task * @param {string} [context.complexityReportPath] - Path to the complexity report file * @param {string} [outputFormat='text'] - Output format ('text' or 'json'). MCP calls should use 'json'. * @returns {Promise<{success: boolean, expandedCount: number, failedCount: number, skippedCount: number, tasksToExpand: number, telemetryData: Array<Object>}>} - Result summary. */ async function expandAllTasks( tasksPath, numSubtasks, // Keep this signature, expandTask handles defaults useResearch = false, additionalContext = '', force = false, // Keep force here for the filter logic context = {}, outputFormat = 'text' // Assume text default for CLI ) { const { session, mcpLog, projectRoot: providedProjectRoot, tag, complexityReportPath } = context; const isMCPCall = !!mcpLog; // Determine if called from MCP const projectRoot = providedProjectRoot || findProjectRoot(); if (!projectRoot) { throw new Error('Could not determine project root directory'); } // Use mcpLog if available, otherwise use the default console log wrapper respecting silent mode const logger = mcpLog || (outputFormat === 'json' ? { // Basic logger for JSON output mode info: (msg) => {}, warn: (msg) => {}, error: (msg) => console.error(`ERROR: ${msg}`), // Still log errors debug: (msg) => {} } : { // CLI logger respecting silent mode info: (msg) => !isSilentMode() && log('info', msg), warn: (msg) => !isSilentMode() && log('warn', msg), error: (msg) => !isSilentMode() && log('error', msg), debug: (msg) => !isSilentMode() && getDebugFlag(session) && log('debug', msg) }); let loadingIndicator = null; let expandedCount = 0; let failedCount = 0; let tasksToExpandCount = 0; const allTelemetryData = []; // Still collect individual data first if (!isMCPCall && outputFormat === 'text') { loadingIndicator = startLoadingIndicator( 'Analyzing tasks for expansion...' ); } try { logger.info(`Reading tasks from ${tasksPath}`); const data = readJSON(tasksPath, projectRoot, tag); if (!data || !data.tasks) { throw new Error(`Invalid tasks data in ${tasksPath}`); } // --- Restore Original Filtering Logic --- const tasksToExpand = data.tasks.filter( (task) => (task.status === 'pending' || task.status === 'in-progress') && // Include 'in-progress' (!task.subtasks || task.subtasks.length === 0 || force) // Check subtasks/force here ); tasksToExpandCount = tasksToExpand.length; // Get the count from the filtered array logger.info(`Found ${tasksToExpandCount} tasks eligible for expansion.`); // --- End Restored Filtering Logic --- if (loadingIndicator) { stopLoadingIndicator(loadingIndicator, 'Analysis complete.'); } if (tasksToExpandCount === 0) { logger.info('No tasks eligible for expansion.'); // --- Fix: Restore success: true and add message --- return { success: true, // Indicate overall success despite no action expandedCount: 0, failedCount: 0, skippedCount: 0, tasksToExpand: 0, telemetryData: allTelemetryData, message: 'No tasks eligible for expansion.' }; // --- End Fix --- } // Iterate over the already filtered tasks for (const task of tasksToExpand) { // Start indicator for individual task expansion in CLI mode let taskIndicator = null; if (!isMCPCall && outputFormat === 'text') { taskIndicator = startLoadingIndicator(`Expanding task ${task.id}...`); } try { // Call the refactored expandTask function AND capture result const result = await expandTask( tasksPath, task.id, numSubtasks, useResearch, additionalContext, { ...context, projectRoot, tag: data.tag || tag, complexityReportPath }, // Pass the whole context object with projectRoot and resolved tag force ); expandedCount++; // Collect individual telemetry data if (result && result.telemetryData) { allTelemetryData.push(result.telemetryData); } if (taskIndicator) { stopLoadingIndicator(taskIndicator, `Task ${task.id} expanded.`); } logger.info(`Successfully expanded task ${task.id}.`); } catch (error) { failedCount++; if (taskIndicator) { stopLoadingIndicator( taskIndicator, `Failed to expand task ${task.id}.`, false ); } logger.error(`Failed to expand task ${task.id}: ${error.message}`); // Continue to the next task } } // --- AGGREGATION AND DISPLAY --- logger.info( `Expansion complete: ${expandedCount} expanded, ${failedCount} failed.` ); // Aggregate the collected telemetry data const aggregatedTelemetryData = aggregateTelemetry( allTelemetryData, 'expand-all-tasks' ); if (outputFormat === 'text') { const summaryContent = `${chalk.white.bold('Expansion Summary:')}\n\n` + `${chalk.cyan('-')} Attempted: ${chalk.bold(tasksToExpandCount)}\n` + `${chalk.green('-')} Expanded: ${chalk.bold(expandedCount)}\n` + // Skipped count is always 0 now due to pre-filtering `${chalk.gray('-')} Skipped: ${chalk.bold(0)}\n` + `${chalk.red('-')} Failed: ${chalk.bold(failedCount)}`; console.log( boxen(summaryContent, { padding: 1, margin: { top: 1 }, borderColor: failedCount > 0 ? 'red' : 'green', // Red if failures, green otherwise borderStyle: 'round' }) ); } if (outputFormat === 'text' && aggregatedTelemetryData) { displayAiUsageSummary(aggregatedTelemetryData, 'cli'); } // Return summary including the AGGREGATED telemetry data return { success: true, expandedCount, failedCount, skippedCount: 0, tasksToExpand: tasksToExpandCount, telemetryData: aggregatedTelemetryData }; } catch (error) { if (loadingIndicator) stopLoadingIndicator(loadingIndicator, 'Error.', false); logger.error(`Error during expand all operation: ${error.message}`); if (!isMCPCall && getDebugFlag(session)) { console.error(error); // Log full stack in debug CLI mode } // Re-throw error for the caller to handle, the direct function will format it throw error; // Let direct function wrapper handle formatting /* Original re-throw: throw new Error(`Failed to expand all tasks: ${error.message}`); */ } } export default expandAllTasks; ``` -------------------------------------------------------------------------------- /tests/unit/ai-providers/openai.test.js: -------------------------------------------------------------------------------- ```javascript /** * Tests for OpenAI Provider - Token parameter handling for GPT-5 * * This test suite covers: * 1. Correct identification of GPT-5 models requiring max_completion_tokens * 2. Token parameter preparation for different model types * 3. Validation of maxTokens parameter * 4. Integer coercion of token values */ import { jest } from '@jest/globals'; // Mock the utils module to prevent logging during tests jest.mock('../../../scripts/modules/utils.js', () => ({ log: jest.fn() })); // Import the provider import { OpenAIProvider } from '../../../src/ai-providers/openai.js'; describe('OpenAIProvider', () => { let provider; beforeEach(() => { provider = new OpenAIProvider(); jest.clearAllMocks(); }); describe('requiresMaxCompletionTokens', () => { it('should return true for GPT-5 models', () => { expect(provider.requiresMaxCompletionTokens('gpt-5')).toBe(true); expect(provider.requiresMaxCompletionTokens('gpt-5-mini')).toBe(true); expect(provider.requiresMaxCompletionTokens('gpt-5-nano')).toBe(true); expect(provider.requiresMaxCompletionTokens('gpt-5-turbo')).toBe(true); }); it('should return false for non-GPT-5 models', () => { expect(provider.requiresMaxCompletionTokens('gpt-4')).toBe(false); expect(provider.requiresMaxCompletionTokens('gpt-4o')).toBe(false); expect(provider.requiresMaxCompletionTokens('gpt-3.5-turbo')).toBe(false); expect(provider.requiresMaxCompletionTokens('o1')).toBe(false); expect(provider.requiresMaxCompletionTokens('o1-mini')).toBe(false); }); it('should handle null/undefined modelId', () => { expect(provider.requiresMaxCompletionTokens(null)).toBeFalsy(); expect(provider.requiresMaxCompletionTokens(undefined)).toBeFalsy(); expect(provider.requiresMaxCompletionTokens('')).toBeFalsy(); }); }); describe('prepareTokenParam', () => { it('should return max_completion_tokens for GPT-5 models', () => { const result = provider.prepareTokenParam('gpt-5', 1000); expect(result).toEqual({ max_completion_tokens: 1000 }); }); it('should return maxTokens for non-GPT-5 models', () => { const result = provider.prepareTokenParam('gpt-4', 1000); expect(result).toEqual({ maxTokens: 1000 }); }); it('should coerce token value to integer', () => { // Float values const result1 = provider.prepareTokenParam('gpt-5', 1000.7); expect(result1).toEqual({ max_completion_tokens: 1000 }); const result2 = provider.prepareTokenParam('gpt-4', 1000.7); expect(result2).toEqual({ maxTokens: 1000 }); // String float const result3 = provider.prepareTokenParam('gpt-5', '1000.7'); expect(result3).toEqual({ max_completion_tokens: 1000 }); // String integers (common CLI input path) expect(provider.prepareTokenParam('gpt-5', '1000')).toEqual({ max_completion_tokens: 1000 }); expect(provider.prepareTokenParam('gpt-4', '1000')).toEqual({ maxTokens: 1000 }); }); it('should return empty object for undefined maxTokens', () => { const result = provider.prepareTokenParam('gpt-5', undefined); expect(result).toEqual({}); }); it('should handle edge cases', () => { // Test with 0 (should still pass through as 0) const result1 = provider.prepareTokenParam('gpt-5', 0); expect(result1).toEqual({ max_completion_tokens: 0 }); // Test with string number const result2 = provider.prepareTokenParam('gpt-5', '100'); expect(result2).toEqual({ max_completion_tokens: 100 }); // Test with negative number (will be floored, validation happens elsewhere) const result3 = provider.prepareTokenParam('gpt-4', -10.5); expect(result3).toEqual({ maxTokens: -11 }); }); }); describe('validateOptionalParams', () => { it('should accept valid maxTokens values', () => { expect(() => provider.validateOptionalParams({ maxTokens: 1000 }) ).not.toThrow(); expect(() => provider.validateOptionalParams({ maxTokens: 1 }) ).not.toThrow(); expect(() => provider.validateOptionalParams({ maxTokens: '1000' }) ).not.toThrow(); }); it('should reject invalid maxTokens values', () => { expect(() => provider.validateOptionalParams({ maxTokens: 0 })).toThrow( Error ); expect(() => provider.validateOptionalParams({ maxTokens: -1 })).toThrow( Error ); expect(() => provider.validateOptionalParams({ maxTokens: NaN })).toThrow( Error ); expect(() => provider.validateOptionalParams({ maxTokens: Infinity }) ).toThrow(Error); expect(() => provider.validateOptionalParams({ maxTokens: 'invalid' }) ).toThrow(Error); }); it('should accept valid temperature values', () => { expect(() => provider.validateOptionalParams({ temperature: 0 }) ).not.toThrow(); expect(() => provider.validateOptionalParams({ temperature: 0.5 }) ).not.toThrow(); expect(() => provider.validateOptionalParams({ temperature: 1 }) ).not.toThrow(); }); it('should reject invalid temperature values', () => { expect(() => provider.validateOptionalParams({ temperature: -0.1 }) ).toThrow(Error); expect(() => provider.validateOptionalParams({ temperature: 1.1 }) ).toThrow(Error); }); }); describe('getRequiredApiKeyName', () => { it('should return OPENAI_API_KEY', () => { expect(provider.getRequiredApiKeyName()).toBe('OPENAI_API_KEY'); }); }); describe('getClient', () => { it('should throw error if API key is missing', () => { expect(() => provider.getClient({})).toThrow(Error); }); it('should create client with apiKey only', () => { const params = { apiKey: 'sk-test-123' }; // The getClient method should return a function const client = provider.getClient(params); expect(typeof client).toBe('function'); // The client function should be callable and return a model object const model = client('gpt-4'); expect(model).toBeDefined(); expect(model.modelId).toBe('gpt-4'); }); it('should create client with apiKey and baseURL', () => { const params = { apiKey: 'sk-test-456', baseURL: 'https://api.openai.example' }; // Should not throw when baseURL is provided const client = provider.getClient(params); expect(typeof client).toBe('function'); // The client function should be callable and return a model object const model = client('gpt-5'); expect(model).toBeDefined(); expect(model.modelId).toBe('gpt-5'); }); it('should return the same client instance for the same parameters', () => { const params = { apiKey: 'sk-test-789' }; // Multiple calls with same params should work const client1 = provider.getClient(params); const client2 = provider.getClient(params); expect(typeof client1).toBe('function'); expect(typeof client2).toBe('function'); // Both clients should be able to create models const model1 = client1('gpt-4'); const model2 = client2('gpt-4'); expect(model1.modelId).toBe('gpt-4'); expect(model2.modelId).toBe('gpt-4'); }); it('should handle different model IDs correctly', () => { const client = provider.getClient({ apiKey: 'sk-test-models' }); // Test with different models const gpt4 = client('gpt-4'); expect(gpt4.modelId).toBe('gpt-4'); const gpt5 = client('gpt-5'); expect(gpt5.modelId).toBe('gpt-5'); const gpt35 = client('gpt-3.5-turbo'); expect(gpt35.modelId).toBe('gpt-3.5-turbo'); }); }); describe('name property', () => { it('should have OpenAI as the provider name', () => { expect(provider.name).toBe('OpenAI'); }); }); }); ``` -------------------------------------------------------------------------------- /docs/cross-tag-task-movement.md: -------------------------------------------------------------------------------- ```markdown # Cross-Tag Task Movement Task Master now supports moving tasks between different tag contexts, allowing you to organize your work across multiple project contexts, feature branches, or development phases. ## Overview Cross-tag task movement enables you to: - Move tasks between different tag contexts (e.g., from "backlog" to "in-progress") - Handle cross-tag dependencies intelligently - Maintain task relationships across different contexts - Organize work across multiple project phases ## Basic Usage ### Within-Tag Moves Move tasks within the same tag context: ```bash # Move a single task task-master move --from=5 --to=7 # Move a subtask task-master move --from=5.2 --to=7.3 # Move multiple tasks task-master move --from=5,6,7 --to=10,11,12 ``` ### Cross-Tag Moves Move tasks between different tag contexts: ```bash # Basic cross-tag move task-master move --from=5 --from-tag=backlog --to-tag=in-progress # Move multiple tasks task-master move --from=5,6,7 --from-tag=backlog --to-tag=done ``` ## Dependency Resolution When moving tasks between tags, you may encounter cross-tag dependencies. Task Master provides several options to handle these: ### Move with Dependencies Move the main task along with all its dependent tasks: ```bash task-master move --from=5 --from-tag=backlog --to-tag=in-progress --with-dependencies ``` This ensures that all dependent tasks are moved together, maintaining the task relationships. ### Break Dependencies Break cross-tag dependencies and move only the specified task: ```bash task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies ``` This removes the dependency relationships and moves only the specified task. ### Force Move Note: Force moves are no longer supported. Instead, use one of these options: - `--with-dependencies` — move dependents together - `--ignore-dependencies` — break cross-tag dependencies ⚠️ **Warning**: This may break dependency relationships and should be used with caution. ## Error Handling Task Master provides enhanced error messages with specific resolution suggestions: ### Cross-Tag Dependency Conflicts When you encounter dependency conflicts, you'll see: ```text ❌ Cannot move tasks from "backlog" to "in-progress" Cross-tag dependency conflicts detected: • Task 5 depends on 2 (in backlog) • Task 6 depends on 3 (in done) Resolution options: 1. Move with dependencies: task-master move --from=5,6 --from-tag=backlog --to-tag=in-progress --with-dependencies 2. Break dependencies: task-master move --from=5,6 --from-tag=backlog --to-tag=in-progress --ignore-dependencies 3. Validate and fix dependencies: task-master validate-dependencies && task-master fix-dependencies 4. Move dependencies first: task-master move --from=2,3 --from-tag=backlog --to-tag=in-progress 5. After deciding, re-run the move with either --with-dependencies or --ignore-dependencies ``` ### Subtask Movement Restrictions Subtasks cannot be moved directly between tags: ```text ❌ Cannot move subtask 5.2 directly between tags Subtask movement restriction: • Subtasks cannot be moved directly between tags • They must be promoted to full tasks first Resolution options: 1. Promote subtask to full task: task-master remove-subtask --id=5.2 --convert 2. Then move the promoted task: task-master move --from=5 --from-tag=backlog --to-tag=in-progress 3. Or move the parent task with all subtasks: task-master move --from=5 --from-tag=backlog --to-tag=in-progress --with-dependencies ``` ### Invalid Tag Combinations When source and target tags are the same: ```text ❌ Invalid tag combination Error details: • Source tag: "backlog" • Target tag: "backlog" • Reason: Source and target tags are identical Resolution options: 1. Use different tags for cross-tag moves 2. Use within-tag move: task-master move --from=<id> --to=<id> --tag=backlog 3. Check available tags: task-master tags ``` ## Best Practices ### 1. Check Dependencies First Before moving tasks, validate your dependencies: ```bash # Check for dependency issues task-master validate-dependencies # Fix common dependency problems task-master fix-dependencies ``` ### 2. Use Appropriate Flags - **`--with-dependencies`**: When you want to maintain task relationships - **`--ignore-dependencies`**: When you want to break cross-tag dependencies ### 3. Organize by Context Use tags to organize work by: - **Development phases**: `backlog`, `in-progress`, `review`, `done` - **Feature branches**: `feature-auth`, `feature-dashboard` - **Team members**: `alice-tasks`, `bob-tasks` - **Project versions**: `v1.0`, `v2.0` ### 4. Handle Subtasks Properly For subtasks, either: 1. Promote the subtask to a full task first 2. Move the parent task with all subtasks using `--with-dependencies` ## Advanced Usage ### Multiple Task Movement Move multiple tasks at once: ```bash # Move multiple tasks with dependencies task-master move --from=5,6,7 --from-tag=backlog --to-tag=in-progress --with-dependencies # Move multiple tasks, breaking dependencies task-master move --from=5,6,7 --from-tag=backlog --to-tag=in-progress --ignore-dependencies ``` ### Tag Creation Target tags are created automatically if they don't exist: ```bash # This will create the "new-feature" tag if it doesn't exist task-master move --from=5 --from-tag=backlog --to-tag=new-feature ``` ### Current Tag Fallback If `--from-tag` is not provided, the current tag is used: ```bash # Uses current tag as source task-master move --from=5 --to-tag=in-progress ``` ## MCP Integration The cross-tag move functionality is also available through MCP tools: ```javascript // Move task with dependencies await moveTask({ from: "5", fromTag: "backlog", toTag: "in-progress", withDependencies: true }); // Break dependencies await moveTask({ from: "5", fromTag: "backlog", toTag: "in-progress", ignoreDependencies: true }); ``` ## Troubleshooting ### Common Issues 1. **"Source tag not found"**: Check available tags with `task-master tags` 2. **"Task not found"**: Verify task IDs with `task-master list` 3. **"Cross-tag dependency conflicts"**: Use dependency resolution flags 4. **"Cannot move subtask"**: Promote subtask first or move parent task ### Getting Help ```bash # Show move command help task-master move --help # Check available tags task-master tags # Validate dependencies task-master validate-dependencies # Fix dependency issues task-master fix-dependencies ``` ## Examples ### Scenario 1: Moving from Backlog to In-Progress ```bash # Check for dependencies first task-master validate-dependencies # Move with dependencies task-master move --from=5 --from-tag=backlog --to-tag=in-progress --with-dependencies ``` ### Scenario 2: Breaking Dependencies ```bash # Move task, breaking cross-tag dependencies task-master move --from=5 --from-tag=backlog --to-tag=done --ignore-dependencies ``` ### Scenario 3: Force Move Choose one of these options explicitly: ```bash # Move together with dependencies task-master move --from=5 --from-tag=backlog --to-tag=in-progress --with-dependencies # Or break dependencies task-master move --from=5 --from-tag=backlog --to-tag=in-progress --ignore-dependencies ``` ### Scenario 4: Moving Subtasks ```bash # Option 1: Promote subtask first task-master remove-subtask --id=5.2 --convert task-master move --from=5 --from-tag=backlog --to-tag=in-progress # Option 2: Move parent with all subtasks task-master move --from=5 --from-tag=backlog --to-tag=in-progress --with-dependencies ``` ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/remove-task.js: -------------------------------------------------------------------------------- ```javascript import path from 'path'; import * as fs from 'fs'; import { readJSON, writeJSON, log, findTaskById } from '../utils.js'; import generateTaskFiles from './generate-task-files.js'; import taskExists from './task-exists.js'; /** * Removes one or more tasks or subtasks from the tasks file * @param {string} tasksPath - Path to the tasks file * @param {string} taskIds - Comma-separated string of task/subtask IDs to remove (e.g., '5,6.1,7') * @param {Object} context - Context object containing projectRoot and tag information * @param {string} [context.projectRoot] - Project root path * @param {string} [context.tag] - Tag for the task * @returns {Object} Result object with success status, messages, and removed task info */ async function removeTask(tasksPath, taskIds, context = {}) { const { projectRoot, tag } = context; const results = { success: true, messages: [], errors: [], removedTasks: [] }; const taskIdsToRemove = taskIds .split(',') .map((id) => id.trim()) .filter(Boolean); // Remove empty strings if any if (taskIdsToRemove.length === 0) { results.success = false; results.errors.push('No valid task IDs provided.'); return results; } try { // Read the tasks file ONCE before the loop, preserving the full tagged structure const rawData = readJSON(tasksPath, projectRoot, tag); // Read raw data if (!rawData) { throw new Error(`Could not read tasks file at ${tasksPath}`); } // Use the full tagged data if available, otherwise use the data as is const fullTaggedData = rawData._rawTaggedData || rawData; if (!fullTaggedData[tag] || !fullTaggedData[tag].tasks) { throw new Error(`Tag '${tag}' not found or has no tasks.`); } const tasks = fullTaggedData[tag].tasks; // Work with tasks from the correct tag const tasksToDeleteFiles = []; // Collect IDs of main tasks whose files should be deleted for (const taskId of taskIdsToRemove) { // Check if the task ID exists *before* attempting removal if (!taskExists(tasks, taskId)) { const errorMsg = `Task with ID ${taskId} in tag '${tag}' not found or already removed.`; results.errors.push(errorMsg); results.success = false; // Mark overall success as false if any error occurs continue; // Skip to the next ID } try { // Handle subtask removal (e.g., '5.2') if (typeof taskId === 'string' && taskId.includes('.')) { const [parentTaskId, subtaskId] = taskId .split('.') .map((id) => parseInt(id, 10)); // Find the parent task const parentTask = tasks.find((t) => t.id === parentTaskId); if (!parentTask || !parentTask.subtasks) { throw new Error( `Parent task ${parentTaskId} or its subtasks not found for subtask ${taskId}` ); } // Find the subtask to remove const subtaskIndex = parentTask.subtasks.findIndex( (st) => st.id === subtaskId ); if (subtaskIndex === -1) { throw new Error( `Subtask ${subtaskId} not found in parent task ${parentTaskId}` ); } // Store the subtask info before removal const removedSubtask = { ...parentTask.subtasks[subtaskIndex], parentTaskId: parentTaskId }; results.removedTasks.push(removedSubtask); // Remove the subtask from the parent parentTask.subtasks.splice(subtaskIndex, 1); results.messages.push( `Successfully removed subtask ${taskId} from tag '${tag}'` ); } // Handle main task removal else { const taskIdNum = parseInt(taskId, 10); const taskIndex = tasks.findIndex((t) => t.id === taskIdNum); if (taskIndex === -1) { throw new Error(`Task with ID ${taskId} not found in tag '${tag}'`); } // Store the task info before removal const removedTask = tasks[taskIndex]; results.removedTasks.push(removedTask); tasksToDeleteFiles.push(taskIdNum); // Add to list for file deletion // Remove the task from the main array tasks.splice(taskIndex, 1); results.messages.push( `Successfully removed task ${taskId} from tag '${tag}'` ); } } catch (innerError) { // Catch errors specific to processing *this* ID const errorMsg = `Error processing ID ${taskId}: ${innerError.message}`; results.errors.push(errorMsg); results.success = false; log('warn', errorMsg); // Log as warning and continue with next ID } } // End of loop through taskIdsToRemove // --- Post-Loop Operations --- // Only proceed with cleanup and saving if at least one task was potentially removed if (results.removedTasks.length > 0) { const allRemovedIds = new Set( taskIdsToRemove.map((id) => typeof id === 'string' && id.includes('.') ? id : parseInt(id, 10) ) ); // Update the tasks in the current tag of the full data structure fullTaggedData[tag].tasks = tasks; // Remove dependencies from all tags for (const tagName in fullTaggedData) { if ( Object.prototype.hasOwnProperty.call(fullTaggedData, tagName) && fullTaggedData[tagName] && fullTaggedData[tagName].tasks ) { const currentTagTasks = fullTaggedData[tagName].tasks; currentTagTasks.forEach((task) => { if (task.dependencies) { task.dependencies = task.dependencies.filter( (depId) => !allRemovedIds.has(depId) ); } if (task.subtasks) { task.subtasks.forEach((subtask) => { if (subtask.dependencies) { subtask.dependencies = subtask.dependencies.filter( (depId) => !allRemovedIds.has(`${task.id}.${depId}`) && !allRemovedIds.has(depId) ); } }); } }); } } // Save the updated raw data structure writeJSON(tasksPath, fullTaggedData, projectRoot, tag); // Delete task files AFTER saving tasks.json for (const taskIdNum of tasksToDeleteFiles) { const taskFileName = path.join( path.dirname(tasksPath), `task_${taskIdNum.toString().padStart(3, '0')}.txt` ); if (fs.existsSync(taskFileName)) { try { fs.unlinkSync(taskFileName); results.messages.push(`Deleted task file: ${taskFileName}`); } catch (unlinkError) { const unlinkMsg = `Failed to delete task file ${taskFileName}: ${unlinkError.message}`; results.errors.push(unlinkMsg); results.success = false; log('warn', unlinkMsg); } } } // Generate updated task files ONCE, with context // try { // await generateTaskFiles(tasksPath, path.dirname(tasksPath), { // projectRoot, // tag // }); // results.messages.push('Task files regenerated successfully.'); // } catch (genError) { // const genErrMsg = `Failed to regenerate task files: ${genError.message}`; // results.errors.push(genErrMsg); // results.success = false; // log('warn', genErrMsg); // } } else if (results.errors.length === 0) { results.messages.push('No tasks found matching the provided IDs.'); } // Consolidate messages for final output const finalMessage = results.messages.join('\n'); const finalError = results.errors.join('\n'); return { success: results.success, message: finalMessage || 'No tasks were removed.', error: finalError || null, removedTasks: results.removedTasks }; } catch (error) { // Catch errors from reading file or other initial setup log('error', `Error removing tasks: ${error.message}`); return { success: false, message: '', error: `Operation failed: ${error.message}`, removedTasks: [] }; } } export default removeTask; ``` -------------------------------------------------------------------------------- /apps/cli/src/commands/set-status.command.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview SetStatusCommand using Commander's native class pattern * Extends Commander.Command for better integration with the framework */ import { Command } from 'commander'; import chalk from 'chalk'; import boxen from 'boxen'; import { createTaskMasterCore, type TaskMasterCore, type TaskStatus } from '@tm/core'; import type { StorageType } from '@tm/core/types'; /** * Valid task status values for validation */ const VALID_TASK_STATUSES: TaskStatus[] = [ 'pending', 'in-progress', 'done', 'deferred', 'cancelled', 'blocked', 'review' ]; /** * Options interface for the set-status command */ export interface SetStatusCommandOptions { id?: string; status?: TaskStatus; format?: 'text' | 'json'; silent?: boolean; project?: string; } /** * Result type from set-status command */ export interface SetStatusResult { success: boolean; updatedTasks: Array<{ taskId: string; oldStatus: TaskStatus; newStatus: TaskStatus; }>; storageType: Exclude<StorageType, 'auto'>; } /** * SetStatusCommand extending Commander's Command class * This is a thin presentation layer over @tm/core */ export class SetStatusCommand extends Command { private tmCore?: TaskMasterCore; private lastResult?: SetStatusResult; constructor(name?: string) { super(name || 'set-status'); // Configure the command this.description('Update the status of one or more tasks') .requiredOption( '-i, --id <id>', 'Task ID(s) to update (comma-separated for multiple, supports subtasks like 5.2)' ) .requiredOption( '-s, --status <status>', `New status (${VALID_TASK_STATUSES.join(', ')})` ) .option('-f, --format <format>', 'Output format (text, json)', 'text') .option('--silent', 'Suppress output (useful for programmatic usage)') .option('-p, --project <path>', 'Project root directory', process.cwd()) .action(async (options: SetStatusCommandOptions) => { await this.executeCommand(options); }); } /** * Execute the set-status command */ private async executeCommand( options: SetStatusCommandOptions ): Promise<void> { try { // Validate required options if (!options.id) { console.error(chalk.red('Error: Task ID is required. Use -i or --id')); process.exit(1); } if (!options.status) { console.error( chalk.red('Error: Status is required. Use -s or --status') ); process.exit(1); } // Validate status if (!VALID_TASK_STATUSES.includes(options.status)) { console.error( chalk.red( `Error: Invalid status "${options.status}". Valid options: ${VALID_TASK_STATUSES.join(', ')}` ) ); process.exit(1); } // Initialize TaskMaster core this.tmCore = await createTaskMasterCore({ projectPath: options.project || process.cwd() }); // Parse task IDs (handle comma-separated values) const taskIds = options.id.split(',').map((id) => id.trim()); // Update each task const updatedTasks: Array<{ taskId: string; oldStatus: TaskStatus; newStatus: TaskStatus; }> = []; for (const taskId of taskIds) { try { const result = await this.tmCore.updateTaskStatus( taskId, options.status ); updatedTasks.push({ taskId: result.taskId, oldStatus: result.oldStatus, newStatus: result.newStatus }); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (!options.silent) { console.error( chalk.red(`Failed to update task ${taskId}: ${errorMessage}`) ); } if (options.format === 'json') { console.log( JSON.stringify({ success: false, error: errorMessage, taskId, timestamp: new Date().toISOString() }) ); } process.exit(1); } } // Store result for potential reuse this.lastResult = { success: true, updatedTasks, storageType: this.tmCore.getStorageType() as Exclude< StorageType, 'auto' > }; // Display results this.displayResults(this.lastResult, options); } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; if (!options.silent) { console.error(chalk.red(`Error: ${errorMessage}`)); } if (options.format === 'json') { console.log(JSON.stringify({ success: false, error: errorMessage })); } process.exit(1); } finally { // Clean up resources if (this.tmCore) { await this.tmCore.close(); } } } /** * Display results based on format */ private displayResults( result: SetStatusResult, options: SetStatusCommandOptions ): void { const format = options.format || 'text'; switch (format) { case 'json': console.log(JSON.stringify(result, null, 2)); break; case 'text': default: if (!options.silent) { this.displayTextResults(result); } break; } } /** * Display results in text format */ private displayTextResults(result: SetStatusResult): void { if (result.updatedTasks.length === 1) { // Single task update const update = result.updatedTasks[0]; console.log( boxen( chalk.white.bold(`✅ Successfully updated task ${update.taskId}`) + '\n\n' + `${chalk.blue('From:')} ${this.getStatusDisplay(update.oldStatus)}\n` + `${chalk.blue('To:')} ${this.getStatusDisplay(update.newStatus)}`, { padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } } ) ); } else { // Multiple task updates console.log( boxen( chalk.white.bold( `✅ Successfully updated ${result.updatedTasks.length} tasks` ) + '\n\n' + result.updatedTasks .map( (update) => `${chalk.cyan(update.taskId)}: ${this.getStatusDisplay(update.oldStatus)} → ${this.getStatusDisplay(update.newStatus)}` ) .join('\n'), { padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } } ) ); } // Show storage info console.log(chalk.gray(`\nUsing ${result.storageType} storage`)); } /** * Get colored status display */ private getStatusDisplay(status: TaskStatus): string { const statusColors: Record<TaskStatus, (text: string) => string> = { pending: chalk.yellow, 'in-progress': chalk.blue, done: chalk.green, deferred: chalk.gray, cancelled: chalk.red, blocked: chalk.red, review: chalk.magenta, completed: chalk.green }; const colorFn = statusColors[status] || chalk.white; return colorFn(status); } /** * Get the last command result (useful for testing or chaining) */ getLastResult(): SetStatusResult | undefined { return this.lastResult; } /** * Static method to register this command on an existing program * This is for gradual migration - allows commands.js to use this */ static registerOn(program: Command): Command { const setStatusCommand = new SetStatusCommand(); program.addCommand(setStatusCommand); return setStatusCommand; } /** * Alternative registration that returns the command for chaining * Can also configure the command name if needed */ static register(program: Command, name?: string): SetStatusCommand { const setStatusCommand = new SetStatusCommand(name); program.addCommand(setStatusCommand); return setStatusCommand; } } /** * Factory function to create and configure the set-status command */ export function createSetStatusCommand(): SetStatusCommand { return new SetStatusCommand(); } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/auth/auth-manager.ts: -------------------------------------------------------------------------------- ```typescript /** * Authentication manager for Task Master CLI */ import { AuthCredentials, OAuthFlowOptions, AuthenticationError, AuthConfig, UserContext } from './types.js'; import { CredentialStore } from './credential-store.js'; import { OAuthService } from './oauth-service.js'; import { SupabaseAuthClient } from '../clients/supabase-client.js'; import { OrganizationService, type Organization, type Brief, type RemoteTask } from '../services/organization.service.js'; import { getLogger } from '../logger/index.js'; /** * Authentication manager class */ export class AuthManager { private static instance: AuthManager | null = null; private credentialStore: CredentialStore; private oauthService: OAuthService; private supabaseClient: SupabaseAuthClient; private organizationService?: OrganizationService; private constructor(config?: Partial<AuthConfig>) { this.credentialStore = CredentialStore.getInstance(config); this.supabaseClient = new SupabaseAuthClient(); this.oauthService = new OAuthService(this.credentialStore, config); // Initialize Supabase client with session restoration this.initializeSupabaseSession(); } /** * Initialize Supabase session from stored credentials */ private async initializeSupabaseSession(): Promise<void> { try { await this.supabaseClient.initialize(); } catch (error) { // Log but don't throw - session might not exist yet const logger = getLogger('AuthManager'); logger.debug('No existing session to restore'); } } /** * Get singleton instance */ static getInstance(config?: Partial<AuthConfig>): AuthManager { if (!AuthManager.instance) { AuthManager.instance = new AuthManager(config); } else if (config) { // Warn if config is provided after initialization const logger = getLogger('AuthManager'); logger.warn( 'getInstance called with config after initialization; config is ignored.' ); } return AuthManager.instance; } /** * Reset the singleton instance (useful for testing) */ static resetInstance(): void { AuthManager.instance = null; CredentialStore.resetInstance(); } /** * Get stored authentication credentials */ getCredentials(): AuthCredentials | null { return this.credentialStore.getCredentials(); } /** * Start OAuth 2.0 Authorization Code Flow with browser handling */ async authenticateWithOAuth( options: OAuthFlowOptions = {} ): Promise<AuthCredentials> { return this.oauthService.authenticate(options); } /** * Get the authorization URL (for browser opening) */ getAuthorizationUrl(): string | null { return this.oauthService.getAuthorizationUrl(); } /** * Refresh authentication token using Supabase session */ async refreshToken(): Promise<AuthCredentials> { try { // Use Supabase's built-in session refresh const session = await this.supabaseClient.refreshSession(); if (!session) { throw new AuthenticationError( 'Failed to refresh session', 'REFRESH_FAILED' ); } // Get existing credentials to preserve context const existingCredentials = this.credentialStore.getCredentials({ allowExpired: true }); // Update authentication data from session const newAuthData: AuthCredentials = { token: session.access_token, refreshToken: session.refresh_token, userId: session.user.id, email: session.user.email, expiresAt: session.expires_at ? new Date(session.expires_at * 1000).toISOString() : undefined, savedAt: new Date().toISOString(), selectedContext: existingCredentials?.selectedContext }; this.credentialStore.saveCredentials(newAuthData); return newAuthData; } catch (error) { if (error instanceof AuthenticationError) { throw error; } throw new AuthenticationError( `Token refresh failed: ${(error as Error).message}`, 'REFRESH_FAILED' ); } } /** * Logout and clear credentials */ async logout(): Promise<void> { try { // First try to sign out from Supabase to revoke tokens await this.supabaseClient.signOut(); } catch (error) { // Log but don't throw - we still want to clear local credentials getLogger('AuthManager').warn('Failed to sign out from Supabase:', error); } // Always clear local credentials (removes auth.json file) this.credentialStore.clearCredentials(); } /** * Check if authenticated */ isAuthenticated(): boolean { return this.credentialStore.hasValidCredentials(); } /** * Get the current user context (org/brief selection) */ getContext(): UserContext | null { const credentials = this.getCredentials(); return credentials?.selectedContext || null; } /** * Update the user context (org/brief selection) */ async updateContext(context: Partial<UserContext>): Promise<void> { const credentials = this.getCredentials(); if (!credentials) { throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED'); } // Merge with existing context const existingContext = credentials.selectedContext || {}; const newContext: UserContext = { ...existingContext, ...context, updatedAt: new Date().toISOString() }; // Save updated credentials with new context const updatedCredentials: AuthCredentials = { ...credentials, selectedContext: newContext }; this.credentialStore.saveCredentials(updatedCredentials); } /** * Clear the user context */ async clearContext(): Promise<void> { const credentials = this.getCredentials(); if (!credentials) { throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED'); } // Remove context from credentials const { selectedContext, ...credentialsWithoutContext } = credentials; this.credentialStore.saveCredentials(credentialsWithoutContext); } /** * Get the organization service instance * Uses the Supabase client with the current session or token */ private async getOrganizationService(): Promise<OrganizationService> { if (!this.organizationService) { // First check if we have credentials with a token const credentials = this.getCredentials(); if (!credentials || !credentials.token) { throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED'); } // Initialize session if needed (this will load from our storage adapter) await this.supabaseClient.initialize(); // Use the SupabaseAuthClient which now has the session const supabaseClient = this.supabaseClient.getClient(); this.organizationService = new OrganizationService(supabaseClient as any); } return this.organizationService; } /** * Get all organizations for the authenticated user */ async getOrganizations(): Promise<Organization[]> { const service = await this.getOrganizationService(); return service.getOrganizations(); } /** * Get all briefs for a specific organization */ async getBriefs(orgId: string): Promise<Brief[]> { const service = await this.getOrganizationService(); return service.getBriefs(orgId); } /** * Get a specific organization by ID */ async getOrganization(orgId: string): Promise<Organization | null> { const service = await this.getOrganizationService(); return service.getOrganization(orgId); } /** * Get a specific brief by ID */ async getBrief(briefId: string): Promise<Brief | null> { const service = await this.getOrganizationService(); return service.getBrief(briefId); } /** * Get all tasks for a specific brief */ async getTasks(briefId: string): Promise<RemoteTask[]> { const service = await this.getOrganizationService(); return service.getTasks(briefId); } } ``` -------------------------------------------------------------------------------- /apps/docs/getting-started/contribute.mdx: -------------------------------------------------------------------------------- ```markdown # Contributing to Task Master Thank you for your interest in contributing to Task Master! We're excited to work with you and appreciate your help in making this project better. 🚀 ## 🤝 Our Collaborative Approach We're a **PR-friendly team** that values collaboration: - ✅ **We review PRs quickly** - Usually within hours, not days - ✅ **We're super reactive** - Expect fast feedback and engagement - ✅ **We sometimes take over PRs** - If your contribution is valuable but needs cleanup, we might jump in to help finish it - ✅ **We're open to all contributions** - From bug fixes to major features **We don't mind AI-generated code**, but we do expect you to: - ✅ **Review and understand** what the AI generated - ✅ **Test the code thoroughly** before submitting - ✅ **Ensure it's well-written** and follows our patterns - ❌ **Don't submit "AI slop"** - untested, unreviewed AI output > **Why this matters**: We spend significant time reviewing PRs. Help us help you by submitting quality contributions that save everyone time! ## 🚀 Quick Start for Contributors ### 1. Fork and Clone ```bash git clone https://github.com/YOUR_USERNAME/claude-task-master.git cd claude-task-master npm install ``` ### 2. Create a Feature Branch **Important**: Always target the `next` branch, not `main`: ```bash git checkout next git pull origin next git checkout -b feature/your-feature-name ``` ### 3. Make Your Changes Follow our development guidelines below. ### 4. Test Everything Yourself **Before submitting your PR**, ensure: ```bash # Run all tests npm test # Check formatting npm run format-check # Fix formatting if needed npm run format ``` ### 5. Create a Changeset **Required for most changes**: ```bash npm run changeset ``` See the [Changeset Guidelines](#changeset-guidelines) below for details. ### 6. Submit Your PR - Target the `next` branch - Write a clear description - Reference any related issues ## 📋 Development Guidelines ### Branch Strategy - **`main`**: Production-ready code - **`next`**: Development branch - **target this for PRs** - **Feature branches**: `feature/description` or `fix/description` ### Code Quality Standards 1. **Write tests** for new functionality 2. **Follow existing patterns** in the codebase 3. **Add JSDoc comments** for functions 4. **Keep functions focused** and single-purpose ### Testing Requirements Your PR **must pass all CI checks**: - ✅ **Unit tests**: `npm test` - ✅ **Format check**: `npm run format-check` **Test your changes locally first** - this saves review time and shows you care about quality. ## 📦 Changeset Guidelines We use [Changesets](https://github.com/changesets/changesets) to manage versioning and generate changelogs. ### When to Create a Changeset **Always create a changeset for**: - ✅ New features - ✅ Bug fixes - ✅ Breaking changes - ✅ Performance improvements - ✅ User-facing documentation updates - ✅ Dependency updates that affect functionality **Skip changesets for**: - ❌ Internal documentation only - ❌ Test-only changes - ❌ Code formatting/linting - ❌ Development tooling that doesn't affect users ### How to Create a Changeset 1. **After making your changes**: ```bash npm run changeset ``` 2. **Choose the bump type**: - **Major**: Breaking changes - **Minor**: New features - **Patch**: Bug fixes, docs, performance improvements 3. **Write a clear summary**: ``` Add support for custom AI models in MCP configuration ``` 4. **Commit the changeset file** with your changes: ```bash git add .changeset/*.md git commit -m "feat: add custom AI model support" ``` ### Changeset vs Git Commit Messages - **Changeset summary**: User-facing, goes in CHANGELOG.md - **Git commit**: Developer-facing, explains the technical change Example: ```bash # Changeset summary (user-facing) "Add support for custom Ollama models" # Git commit message (developer-facing) "feat(models): implement custom Ollama model validation - Add model validation for custom Ollama endpoints - Update configuration schema to support custom models - Add tests for new validation logic" ``` ## 🔧 Development Setup ### Prerequisites - Node.js 18+ - npm or yarn ### Environment Setup 1. **Copy environment template**: ```bash cp .env.example .env ``` 2. **Add your API keys** (for testing AI features): ```bash ANTHROPIC_API_KEY=your_key_here OPENAI_API_KEY=your_key_here # Add others as needed ``` ### Running Tests ```bash # Run all tests npm test # Run tests in watch mode npm run test:watch # Run with coverage npm run test:coverage # Run E2E tests npm run test:e2e ``` ### Code Formatting We use Prettier for consistent formatting: ```bash # Check formatting npm run format-check # Fix formatting npm run format ``` ## 📝 PR Guidelines ### Before Submitting - [ ] **Target the `next` branch** - [ ] **Test everything locally** - [ ] **Run the full test suite** - [ ] **Check code formatting** - [ ] **Create a changeset** (if needed) - [ ] **Re-read your changes** - ensure they're clean and well-thought-out ### PR Description Template ```markdown ## Description Brief description of what this PR does. ## Type of Change - [ ] Bug fix - [ ] New feature - [ ] Breaking change - [ ] Documentation update ## Testing - [ ] I have tested this locally - [ ] All existing tests pass - [ ] I have added tests for new functionality ## Changeset - [ ] I have created a changeset (or this change doesn't need one) ## Additional Notes Any additional context or notes for reviewers. ``` ### What We Look For ✅ **Good PRs**: - Clear, focused changes - Comprehensive testing - Good commit messages - Proper changeset (when needed) - Self-reviewed code ❌ **Avoid**: - Massive PRs that change everything - Untested code - Formatting issues - Missing changesets for user-facing changes - AI-generated code that wasn't reviewed ## 🏗️ Project Structure ``` claude-task-master/ ├── bin/ # CLI executables ├── mcp-server/ # MCP server implementation ├── scripts/ # Core task management logic ├── src/ # Shared utilities and providers and well refactored code (we are slowly moving everything here) ├── tests/ # Test files ├── docs/ # Documentation └── .cursor/ # Cursor IDE rules and configuration └── assets/ # Assets like rules and configuration for all IDEs ``` ### Key Areas for Contribution - **CLI Commands**: `scripts/modules/commands.js` - **MCP Tools**: `mcp-server/src/tools/` - **Core Logic**: `scripts/modules/task-manager/` - **AI Providers**: `src/ai-providers/` - **Tests**: `tests/` ## 🐛 Reporting Issues ### Bug Reports Include: - Task Master version - Node.js version - Operating system - Steps to reproduce - Expected vs actual behavior - Error messages/logs ### Feature Requests Include: - Clear description of the feature - Use case/motivation - Proposed implementation (if you have ideas) - Willingness to contribute ## 💬 Getting Help - **Discord**: [Join our community](https://discord.gg/taskmasterai) - **Issues**: [GitHub Issues](https://github.com/eyaltoledano/claude-task-master/issues) - **Discussions**: [GitHub Discussions](https://github.com/eyaltoledano/claude-task-master/discussions) ## 📄 License By contributing, you agree that your contributions will be licensed under the same license as the project (MIT with Commons Clause). --- **Thank you for contributing to Task Master!** 🎉 Your contributions help make AI-driven development more accessible and efficient for everyone. ``` -------------------------------------------------------------------------------- /packages/tm-core/src/storage/storage-factory.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview Storage factory for creating appropriate storage implementations */ import type { IStorage } from '../interfaces/storage.interface.js'; import type { IConfiguration, RuntimeStorageConfig, StorageSettings } from '../interfaces/configuration.interface.js'; import { FileStorage } from './file-storage/index.js'; import { ApiStorage } from './api-storage.js'; import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js'; import { AuthManager } from '../auth/auth-manager.js'; import { getLogger } from '../logger/index.js'; import { SupabaseAuthClient } from '../clients/supabase-client.js'; /** * Factory for creating storage implementations based on configuration */ export class StorageFactory { /** * Create a storage implementation from runtime storage config * This is the preferred method when you have a RuntimeStorageConfig * @param storageConfig - Runtime storage configuration * @param projectPath - Project root path (for file storage) * @returns Storage implementation */ static createFromStorageConfig( storageConfig: RuntimeStorageConfig, projectPath: string ): IStorage { // Wrap the storage config in the expected format, including projectPath // This ensures ApiStorage receives the projectPath for projectId return StorageFactory.create( { storage: storageConfig, projectPath } as Partial<IConfiguration>, projectPath ); } /** * Create a storage implementation based on configuration * @param config - Configuration object * @param projectPath - Project root path (for file storage) * @returns Storage implementation */ static create( config: Partial<IConfiguration>, projectPath: string ): IStorage { const storageType = config.storage?.type || 'auto'; const logger = getLogger('StorageFactory'); switch (storageType) { case 'file': logger.debug('📁 Using local file storage'); return StorageFactory.createFileStorage(projectPath, config); case 'api': if (!StorageFactory.isHamsterAvailable(config)) { const missing: string[] = []; if (!config.storage?.apiEndpoint) missing.push('apiEndpoint'); if (!config.storage?.apiAccessToken) missing.push('apiAccessToken'); // Check if authenticated via AuthManager const authManager = AuthManager.getInstance(); if (!authManager.isAuthenticated()) { throw new TaskMasterError( `API storage not fully configured (${missing.join(', ') || 'credentials missing'}). Run: tm auth login, or set the missing field(s).`, ERROR_CODES.MISSING_CONFIGURATION, { storageType: 'api', missing } ); } // Use auth token from AuthManager const credentials = authManager.getCredentials(); if (credentials) { // Merge with existing storage config, ensuring required fields const nextStorage: StorageSettings = { ...(config.storage as StorageSettings), type: 'api', apiAccessToken: credentials.token, apiEndpoint: config.storage?.apiEndpoint || process.env.HAMSTER_API_URL || 'https://tryhamster.com/api' }; config.storage = nextStorage; } } logger.info('☁️ Using API storage'); return StorageFactory.createApiStorage(config); case 'auto': // Auto-detect based on authentication status const authManager = AuthManager.getInstance(); // First check if API credentials are explicitly configured if (StorageFactory.isHamsterAvailable(config)) { logger.info('☁️ Using API storage (configured)'); return StorageFactory.createApiStorage(config); } // Then check if authenticated via AuthManager if (authManager.isAuthenticated()) { const credentials = authManager.getCredentials(); if (credentials) { // Configure API storage with auth credentials const nextStorage: StorageSettings = { ...(config.storage as StorageSettings), type: 'api', apiAccessToken: credentials.token, apiEndpoint: config.storage?.apiEndpoint || process.env.HAMSTER_API_URL || 'https://tryhamster.com/api' }; config.storage = nextStorage; logger.info('☁️ Using API storage (authenticated)'); return StorageFactory.createApiStorage(config); } } // Default to file storage logger.debug('📁 Using local file storage'); return StorageFactory.createFileStorage(projectPath, config); default: throw new TaskMasterError( `Unknown storage type: ${storageType}`, ERROR_CODES.INVALID_INPUT, { storageType } ); } } /** * Create file storage implementation */ private static createFileStorage( projectPath: string, config: Partial<IConfiguration> ): FileStorage { const basePath = config.storage?.basePath || projectPath; return new FileStorage(basePath); } /** * Create API storage implementation */ private static createApiStorage(config: Partial<IConfiguration>): ApiStorage { // Use our SupabaseAuthClient instead of creating a raw Supabase client const supabaseAuthClient = new SupabaseAuthClient(); const supabaseClient = supabaseAuthClient.getClient(); return new ApiStorage({ supabaseClient, projectId: config.projectPath || '', enableRetry: config.retry?.retryOnNetworkError, maxRetries: config.retry?.retryAttempts }); } /** * Detect optimal storage type based on available configuration */ static detectOptimalStorage(config: Partial<IConfiguration>): 'file' | 'api' { // If API credentials are provided, prefer API storage (Hamster) if (config.storage?.apiEndpoint && config.storage?.apiAccessToken) { return 'api'; } // Default to file storage return 'file'; } /** * Validate storage configuration */ static validateStorageConfig(config: Partial<IConfiguration>): { isValid: boolean; errors: string[]; } { const errors: string[] = []; const storageType = config.storage?.type; if (!storageType) { errors.push('Storage type is not specified'); return { isValid: false, errors }; } switch (storageType) { case 'api': if (!config.storage?.apiEndpoint) { errors.push('API endpoint is required for API storage'); } if (!config.storage?.apiAccessToken) { errors.push('API access token is required for API storage'); } break; case 'file': // File storage doesn't require additional config break; case 'auto': // Auto storage is valid - it will determine the actual type at runtime // No specific validation needed as it will fall back to file if API not configured break; default: errors.push(`Unknown storage type: ${storageType}`); } return { isValid: errors.length === 0, errors }; } /** * Check if Hamster (API storage) is available */ static isHamsterAvailable(config: Partial<IConfiguration>): boolean { return !!(config.storage?.apiEndpoint && config.storage?.apiAccessToken); } /** * Create a storage implementation with fallback * Tries API storage first, falls back to file storage */ static async createWithFallback( config: Partial<IConfiguration>, projectPath: string ): Promise<IStorage> { // Try API storage if configured if (StorageFactory.isHamsterAvailable(config)) { try { const apiStorage = StorageFactory.createApiStorage(config); await apiStorage.initialize(); return apiStorage; } catch (error) { const logger = getLogger('StorageFactory'); logger.warn( 'Failed to initialize API storage, falling back to file storage:', error ); } } // Fallback to file storage return StorageFactory.createFileStorage(projectPath, config); } } ``` -------------------------------------------------------------------------------- /mcp-server/src/core/utils/path-utils.js: -------------------------------------------------------------------------------- ```javascript import path from 'path'; import { findTasksPath as coreFindTasksPath, findPRDPath as coreFindPrdPath, findComplexityReportPath as coreFindComplexityReportPath, findProjectRoot as coreFindProjectRoot, normalizeProjectRoot } from '../../../../src/utils/path-utils.js'; import { PROJECT_MARKERS } from '../../../../src/constants/paths.js'; /** * MCP-specific path utilities that extend core path utilities with session support * This module handles session-specific path resolution for the MCP server */ /** * Silent logger for MCP context to prevent console output */ const silentLogger = { info: () => {}, warn: () => {}, error: () => {}, debug: () => {}, success: () => {} }; /** * Cache for last found project root to improve performance */ export const lastFoundProjectRoot = null; /** * Find PRD file with MCP support * @param {string} [explicitPath] - Explicit path to PRD file (highest priority) * @param {Object} [args] - Arguments object for context * @param {Object} [log] - Logger object to prevent console logging * @returns {string|null} - Resolved path to PRD file or null if not found */ export function findPrdPath(explicitPath, args = null, log = silentLogger) { return coreFindPrdPath(explicitPath, args, log); } /** * Resolve tasks.json path from arguments * Prioritizes explicit path parameter, then uses fallback logic * @param {Object} args - Arguments object containing projectRoot and optional file path * @param {Object} [log] - Logger object to prevent console logging * @returns {string|null} - Resolved path to tasks.json or null if not found */ export function resolveTasksPath(args, log = silentLogger) { // Get explicit path from args.file if provided const explicitPath = args?.file; const rawProjectRoot = args?.projectRoot; // If explicit path is provided and absolute, use it directly if (explicitPath && path.isAbsolute(explicitPath)) { return explicitPath; } // Normalize project root if provided const projectRoot = rawProjectRoot ? normalizeProjectRoot(rawProjectRoot) : null; // If explicit path is relative, resolve it relative to normalized projectRoot if (explicitPath && projectRoot) { return path.resolve(projectRoot, explicitPath); } // Use core findTasksPath with explicit path and normalized projectRoot context if (projectRoot) { return coreFindTasksPath(explicitPath, { projectRoot }, log); } // Fallback to core function without projectRoot context return coreFindTasksPath(explicitPath, null, log); } /** * Resolve PRD path from arguments * @param {Object} args - Arguments object containing projectRoot and optional input path * @param {Object} [log] - Logger object to prevent console logging * @returns {string|null} - Resolved path to PRD file or null if not found */ export function resolvePrdPath(args, log = silentLogger) { // Get explicit path from args.input if provided const explicitPath = args?.input; const rawProjectRoot = args?.projectRoot; // If explicit path is provided and absolute, use it directly if (explicitPath && path.isAbsolute(explicitPath)) { return explicitPath; } // Normalize project root if provided const projectRoot = rawProjectRoot ? normalizeProjectRoot(rawProjectRoot) : null; // If explicit path is relative, resolve it relative to normalized projectRoot if (explicitPath && projectRoot) { return path.resolve(projectRoot, explicitPath); } // Use core findPRDPath with explicit path and normalized projectRoot context if (projectRoot) { return coreFindPrdPath(explicitPath, { projectRoot }, log); } // Fallback to core function without projectRoot context return coreFindPrdPath(explicitPath, null, log); } /** * Resolve complexity report path from arguments * @param {Object} args - Arguments object containing projectRoot and optional complexityReport path * @param {Object} [log] - Logger object to prevent console logging * @returns {string|null} - Resolved path to complexity report or null if not found */ export function resolveComplexityReportPath(args, log = silentLogger) { // Get explicit path from args.complexityReport if provided const explicitPath = args?.complexityReport; const rawProjectRoot = args?.projectRoot; const tag = args?.tag; // If explicit path is provided and absolute, use it directly if (explicitPath && path.isAbsolute(explicitPath)) { return explicitPath; } // Normalize project root if provided const projectRoot = rawProjectRoot ? normalizeProjectRoot(rawProjectRoot) : null; // If explicit path is relative, resolve it relative to normalized projectRoot if (explicitPath && projectRoot) { return path.resolve(projectRoot, explicitPath); } // Use core findComplexityReportPath with explicit path and normalized projectRoot context if (projectRoot) { return coreFindComplexityReportPath( explicitPath, { projectRoot, tag }, log ); } // Fallback to core function without projectRoot context return coreFindComplexityReportPath(explicitPath, null, log); } /** * Resolve any project-relative path from arguments * @param {string} relativePath - Relative path to resolve * @param {Object} args - Arguments object containing projectRoot * @returns {string} - Resolved absolute path */ export function resolveProjectPath(relativePath, args) { // Ensure we have a projectRoot from args if (!args?.projectRoot) { throw new Error('projectRoot is required in args to resolve project paths'); } // Normalize the project root to prevent double .taskmaster paths const projectRoot = normalizeProjectRoot(args.projectRoot); // If already absolute, return as-is if (path.isAbsolute(relativePath)) { return relativePath; } // Resolve relative to normalized projectRoot return path.resolve(projectRoot, relativePath); } /** * Find project root using core utility * @param {string} [startDir] - Directory to start searching from * @returns {string|null} - Project root path or null if not found */ export function findProjectRoot(startDir) { return coreFindProjectRoot(startDir); } // MAIN EXPORTS FOR MCP TOOLS - these are the functions MCP tools should use /** * Find tasks.json path from arguments - primary MCP function * @param {Object} args - Arguments object containing projectRoot and optional file path * @param {Object} [log] - Log function to prevent console logging * @returns {string|null} - Resolved path to tasks.json or null if not found */ export function findTasksPath(args, log = silentLogger) { return resolveTasksPath(args, log); } /** * Find complexity report path from arguments - primary MCP function * @param {Object} args - Arguments object containing projectRoot and optional complexityReport path * @param {Object} [log] - Log function to prevent console logging * @returns {string|null} - Resolved path to complexity report or null if not found */ export function findComplexityReportPath(args, log = silentLogger) { return resolveComplexityReportPath(args, log); } /** * Find PRD path - primary MCP function * @param {string} [explicitPath] - Explicit path to PRD file * @param {Object} [args] - Arguments object for context (not used in current implementation) * @param {Object} [log] - Logger object to prevent console logging * @returns {string|null} - Resolved path to PRD file or null if not found */ export function findPRDPath(explicitPath, args = null, log = silentLogger) { return findPrdPath(explicitPath, args, log); } // Legacy aliases for backward compatibility - DEPRECATED export const findTasksJsonPath = findTasksPath; export const findComplexityReportJsonPath = findComplexityReportPath; // Re-export PROJECT_MARKERS for MCP tools that import it from this module export { PROJECT_MARKERS }; ``` -------------------------------------------------------------------------------- /scripts/modules/task-manager/parse-prd/parse-prd.js: -------------------------------------------------------------------------------- ```javascript import chalk from 'chalk'; import { StreamingError, STREAMING_ERROR_CODES } from '../../../../src/utils/stream-parser.js'; import { TimeoutManager } from '../../../../src/utils/timeout-manager.js'; import { getDebugFlag, getDefaultPriority } from '../../config-manager.js'; // Import configuration classes import { PrdParseConfig, LoggingConfig } from './parse-prd-config.js'; // Import helper functions import { readPrdContent, loadExistingTasks, validateFileOperations, processTasks, saveTasksToFile, buildPrompts, displayCliSummary, displayNonStreamingCliOutput } from './parse-prd-helpers.js'; // Import handlers import { handleStreamingService } from './parse-prd-streaming.js'; import { handleNonStreamingService } from './parse-prd-non-streaming.js'; // ============================================================================ // MAIN PARSING FUNCTIONS (Simplified after refactoring) // ============================================================================ /** * Shared parsing logic for both streaming and non-streaming * @param {PrdParseConfig} config - Configuration object * @param {Function} serviceHandler - Handler function for AI service * @param {boolean} isStreaming - Whether this is streaming mode * @returns {Promise<Object>} Result object with success status and telemetry */ async function parsePRDCore(config, serviceHandler, isStreaming) { const logger = new LoggingConfig(config.mcpLog, config.reportProgress); logger.report( `Parsing PRD file: ${config.prdPath}, Force: ${config.force}, Append: ${config.append}, Research: ${config.research}`, 'debug' ); try { // Load existing tasks const { existingTasks, nextId } = loadExistingTasks( config.tasksPath, config.targetTag ); // Validate operations validateFileOperations({ existingTasks, targetTag: config.targetTag, append: config.append, force: config.force, isMCP: config.isMCP, logger }); // Read PRD content and build prompts const prdContent = readPrdContent(config.prdPath); const prompts = await buildPrompts(config, prdContent, nextId); // Call the appropriate service handler const serviceResult = await serviceHandler( config, prompts, config.numTasks ); // Process tasks const defaultPriority = getDefaultPriority(config.projectRoot) || 'medium'; const processedNewTasks = processTasks( serviceResult.parsedTasks, nextId, existingTasks, defaultPriority ); // Combine with existing if appending const finalTasks = config.append ? [...existingTasks, ...processedNewTasks] : processedNewTasks; // Save to file saveTasksToFile(config.tasksPath, finalTasks, config.targetTag, logger); // Handle completion reporting await handleCompletionReporting( config, serviceResult, processedNewTasks, finalTasks, nextId, isStreaming ); return { success: true, tasksPath: config.tasksPath, telemetryData: serviceResult.aiServiceResponse?.telemetryData, tagInfo: serviceResult.aiServiceResponse?.tagInfo }; } catch (error) { logger.report(`Error parsing PRD: ${error.message}`, 'error'); if (!config.isMCP) { console.error(chalk.red(`Error: ${error.message}`)); if (getDebugFlag(config.projectRoot)) { console.error(error); } } throw error; } } /** * Handle completion reporting for both CLI and MCP * @param {PrdParseConfig} config - Configuration object * @param {Object} serviceResult - Result from service handler * @param {Array} processedNewTasks - New tasks that were processed * @param {Array} finalTasks - All tasks after processing * @param {number} nextId - Next available task ID * @param {boolean} isStreaming - Whether this was streaming mode */ async function handleCompletionReporting( config, serviceResult, processedNewTasks, finalTasks, nextId, isStreaming ) { const { aiServiceResponse, estimatedInputTokens, estimatedOutputTokens } = serviceResult; // MCP progress reporting if (config.reportProgress) { const hasValidTelemetry = aiServiceResponse?.telemetryData && (aiServiceResponse.telemetryData.inputTokens > 0 || aiServiceResponse.telemetryData.outputTokens > 0); let completionMessage; if (hasValidTelemetry) { const cost = aiServiceResponse.telemetryData.totalCost || 0; const currency = aiServiceResponse.telemetryData.currency || 'USD'; completionMessage = `✅ Task Generation Completed | Tokens (I/O): ${aiServiceResponse.telemetryData.inputTokens}/${aiServiceResponse.telemetryData.outputTokens} | Cost: ${currency === 'USD' ? '$' : currency}${cost.toFixed(4)}`; } else { const outputTokens = isStreaming ? estimatedOutputTokens : 'unknown'; completionMessage = `✅ Task Generation Completed | ~Tokens (I/O): ${estimatedInputTokens}/${outputTokens} | Cost: ~$0.00`; } await config.reportProgress({ progress: config.numTasks, total: config.numTasks, message: completionMessage }); } // CLI output if (config.outputFormat === 'text' && !config.isMCP) { if (isStreaming && serviceResult.summary) { await displayCliSummary({ processedTasks: processedNewTasks, nextId, summary: serviceResult.summary, prdPath: config.prdPath, tasksPath: config.tasksPath, usedFallback: serviceResult.usedFallback, aiServiceResponse }); } else if (!isStreaming) { displayNonStreamingCliOutput({ processedTasks: processedNewTasks, research: config.research, finalTasks, tasksPath: config.tasksPath, aiServiceResponse }); } } } /** * Parse PRD with streaming progress reporting */ async function parsePRDWithStreaming( prdPath, tasksPath, numTasks, options = {} ) { const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options); return parsePRDCore(config, handleStreamingService, true); } /** * Parse PRD without streaming (fallback) */ async function parsePRDWithoutStreaming( prdPath, tasksPath, numTasks, options = {} ) { const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options); return parsePRDCore(config, handleNonStreamingService, false); } /** * Main entry point - decides between streaming and non-streaming */ async function parsePRD(prdPath, tasksPath, numTasks, options = {}) { const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options); if (config.useStreaming) { try { return await parsePRDWithStreaming(prdPath, tasksPath, numTasks, options); } catch (streamingError) { // Check if this is a streaming-specific error (including timeout) const isStreamingError = streamingError instanceof StreamingError || streamingError.code === STREAMING_ERROR_CODES.NOT_ASYNC_ITERABLE || streamingError.code === STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED || streamingError.code === STREAMING_ERROR_CODES.STREAM_NOT_ITERABLE || TimeoutManager.isTimeoutError(streamingError); if (isStreamingError) { const logger = new LoggingConfig(config.mcpLog, config.reportProgress); // Show fallback message if (config.outputFormat === 'text' && !config.isMCP) { console.log( chalk.yellow( `⚠️ Streaming operation ${streamingError.message.includes('timed out') ? 'timed out' : 'failed'}. Falling back to non-streaming mode...` ) ); } else { logger.report( `Streaming failed (${streamingError.message}), falling back to non-streaming mode...`, 'warn' ); } // Fallback to non-streaming return await parsePRDWithoutStreaming( prdPath, tasksPath, numTasks, options ); } else { throw streamingError; } } } else { return await parsePRDWithoutStreaming( prdPath, tasksPath, numTasks, options ); } } export default parsePRD; ``` -------------------------------------------------------------------------------- /tests/unit/scripts/modules/task-manager/clear-subtasks.test.js: -------------------------------------------------------------------------------- ```javascript /** * Tests for the clear-subtasks.js module */ import { jest } from '@jest/globals'; // Mock the dependencies before importing the module under test jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({ readJSON: jest.fn(), writeJSON: jest.fn(), log: jest.fn(), CONFIG: { model: 'mock-claude-model', maxTokens: 4000, temperature: 0.7, debug: false }, findTaskById: jest.fn(), isSilentMode: jest.fn(() => false), truncate: jest.fn((text) => text), ensureTagMetadata: jest.fn() })); jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({ displayBanner: jest.fn() })); jest.unstable_mockModule( '../../../../../scripts/modules/task-manager/generate-task-files.js', () => ({ default: jest.fn().mockResolvedValue() }) ); // Mock external UI libraries jest.unstable_mockModule('chalk', () => ({ default: { white: { bold: jest.fn((text) => text) }, cyan: Object.assign( jest.fn((text) => text), { bold: jest.fn((text) => text) } ), green: jest.fn((text) => text), yellow: jest.fn((text) => text), bold: jest.fn((text) => text) } })); jest.unstable_mockModule('boxen', () => ({ default: jest.fn((text) => text) })); jest.unstable_mockModule('cli-table3', () => ({ default: jest.fn().mockImplementation(() => ({ push: jest.fn(), toString: jest.fn(() => 'mocked table') })) })); // Mock process.exit to prevent Jest worker crashes const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => { throw new Error(`process.exit called with "${code}"`); }); // Import the mocked modules const { readJSON, writeJSON, log, findTaskById, ensureTagMetadata } = await import('../../../../../scripts/modules/utils.js'); const generateTaskFiles = ( await import( '../../../../../scripts/modules/task-manager/generate-task-files.js' ) ).default; // Import the module under test const { default: clearSubtasks } = await import( '../../../../../scripts/modules/task-manager/clear-subtasks.js' ); describe('clearSubtasks', () => { const sampleTasks = { master: { tasks: [ { id: 1, title: 'Task 1', subtasks: [] }, { id: 2, title: 'Task 2', subtasks: [] }, { id: 3, title: 'Task 3', subtasks: [{ id: 1, title: 'Subtask 3.1' }] }, { id: 4, title: 'Task 4', subtasks: [{ id: 1, title: 'Subtask 4.1' }] } ] } }; beforeEach(() => { jest.clearAllMocks(); mockExit.mockClear(); readJSON.mockImplementation((tasksPath, projectRoot, tag) => { // Ensure tag contract is honoured expect(tag).toBeDefined(); expect(tag).toBe('master'); // Create a deep copy to avoid mutation issues between tests const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks)); // Return the data for the 'master' tag, which is what the tests use return { ...sampleTasksCopy.master, tag: tag || 'master', _rawTaggedData: sampleTasksCopy }; }); writeJSON.mockResolvedValue(); generateTaskFiles.mockResolvedValue(); log.mockImplementation(() => {}); }); test('should clear subtasks from a specific task', () => { // Arrange const taskId = '3'; const tasksPath = 'tasks/tasks.json'; const context = { tag: 'master' }; // Act clearSubtasks(tasksPath, taskId, context); // Assert expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master'); expect(writeJSON).toHaveBeenCalledWith( tasksPath, expect.objectContaining({ _rawTaggedData: expect.objectContaining({ master: expect.objectContaining({ tasks: expect.arrayContaining([ expect.objectContaining({ id: 3, subtasks: [] // Should be empty }) ]) }) }) }), undefined, 'master' ); }); test('should clear subtasks from multiple tasks when given comma-separated IDs', () => { // Arrange const taskIds = '3,4'; const tasksPath = 'tasks/tasks.json'; const context = { tag: 'master' }; // Act clearSubtasks(tasksPath, taskIds, context); // Assert expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master'); expect(writeJSON).toHaveBeenCalledWith( tasksPath, expect.objectContaining({ _rawTaggedData: expect.objectContaining({ master: expect.objectContaining({ tasks: expect.arrayContaining([ expect.objectContaining({ id: 3, subtasks: [] }), expect.objectContaining({ id: 4, subtasks: [] }) ]) }) }) }), undefined, 'master' ); }); test('should handle tasks with no subtasks', () => { // Arrange const taskId = '1'; // Task 1 already has no subtasks const tasksPath = 'tasks/tasks.json'; const context = { tag: 'master' }; // Act clearSubtasks(tasksPath, taskId, context); // Assert expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master'); // Should not write the file if no changes were made expect(writeJSON).not.toHaveBeenCalled(); expect(generateTaskFiles).not.toHaveBeenCalled(); }); test('should handle non-existent task IDs gracefully', () => { // Arrange const taskId = '99'; // Non-existent task const tasksPath = 'tasks/tasks.json'; const context = { tag: 'master' }; // Act clearSubtasks(tasksPath, taskId, context); // Assert expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master'); expect(log).toHaveBeenCalledWith('error', 'Task 99 not found'); // Should not write the file if no changes were made expect(writeJSON).not.toHaveBeenCalled(); expect(generateTaskFiles).not.toHaveBeenCalled(); }); test('should handle multiple task IDs including both valid and non-existent IDs', () => { // Arrange const taskIds = '3,99'; // Mix of valid and invalid IDs const tasksPath = 'tasks/tasks.json'; const context = { tag: 'master' }; // Act clearSubtasks(tasksPath, taskIds, context); // Assert expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master'); expect(log).toHaveBeenCalledWith('error', 'Task 99 not found'); // Since task 3 has subtasks that should be cleared, writeJSON should be called expect(writeJSON).toHaveBeenCalledWith( tasksPath, expect.objectContaining({ tasks: expect.arrayContaining([ expect.objectContaining({ id: 3, subtasks: [] }) ]), tag: 'master', _rawTaggedData: expect.objectContaining({ master: expect.objectContaining({ tasks: expect.arrayContaining([ expect.objectContaining({ id: 3, subtasks: [] }) ]) }) }) }), undefined, 'master' ); }); test('should handle file read errors', () => { // Arrange readJSON.mockImplementation(() => { throw new Error('File read failed'); }); // Act & Assert expect(() => { clearSubtasks('tasks/tasks.json', '3', { tag: 'master' }); }).toThrow('File read failed'); }); test('should handle invalid tasks data', () => { // Arrange readJSON.mockReturnValue(null); // Act & Assert expect(() => { clearSubtasks('tasks/tasks.json', '3', { tag: 'master' }); }).toThrow('process.exit called'); expect(log).toHaveBeenCalledWith('error', 'No valid tasks found.'); }); test('should handle file write errors', () => { // Arrange // Ensure task 3 has subtasks to clear so writeJSON gets called readJSON.mockReturnValue({ ...sampleTasks.master, tag: 'master', _rawTaggedData: sampleTasks, tasks: [ ...sampleTasks.master.tasks.slice(0, 2), { ...sampleTasks.master.tasks[2], subtasks: [{ id: 1, title: 'Subtask to clear' }] }, ...sampleTasks.master.tasks.slice(3) ] }); writeJSON.mockImplementation(() => { throw new Error('File write failed'); }); // Act & Assert expect(() => { clearSubtasks('tasks/tasks.json', '3', { tag: 'master' }); }).toThrow('File write failed'); }); }); ``` -------------------------------------------------------------------------------- /.taskmaster/docs/research/2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md: -------------------------------------------------------------------------------- ```markdown --- title: Research Session query: "how can i improve the scope-up and scope-down command" date: 6/14/2025 time: 4:35:03 PM timestamp: 2025-06-14T20:35:03.231Z exchanges: 2 --- # Research Session ## Initial Query **Question:** how can i improve the scope-up and scope-down command **Response:** ## Improving the `scope-up` and `scope-down` Commands **Clarify Command Purpose and User Experience** - Ensure the commands have clear, intuitive behavior. For example, `scope-up` should consistently broaden the context or task view, while `scope-down` should reliably narrow it. - Provide immediate, user-friendly feedback after each command, indicating what changed (e.g., "Now viewing parent task and all subtasks" or "Focused on subtask: X"). - Document the commands with examples in your `docs/command-reference.md` and `docs/examples.md` to help users understand their effects and use cases. **Enhance Context Awareness** - Make the commands context-sensitive: if the user is already at the top or bottom of the scope hierarchy, return a helpful message instead of failing silently. - Allow optional arguments to specify the exact level or target (e.g., `scope-up 2` to move up two levels, or `scope-down <subtask-id>` to jump directly to a subtask). **Robust Error Handling and Validation** - Implement checks to prevent invalid operations, such as attempting to scope up from the root or scope down when no subtasks exist. - Provide clear error messages and suggestions for next steps, improving user trust and reducing confusion[1][5]. **Integrate with Task and Tag Systems** - Ensure `scope-up` and `scope-down` interact smoothly with your tagging and task hierarchy features. For example, scoping up from a subtask should show the parent task and its context, while scoping down should allow selection from available subtasks or related tags. - Consider supporting batch operations, such as scoping down into all subtasks and displaying them in a summarized view. **Improve Performance and Synchronization** - Optimize the underlying logic to minimize delays, especially in large task trees. Use asynchronous operations where possible to keep the UI responsive. - If the commands trigger data loading or context switching, ensure synchronization so that subsequent commands operate on the correct, updated context[2][4]. **Testing and Stakeholder Feedback** - Add or expand unit and integration tests for these commands (see `tests/unit/commands.test.js` and `integration/cli/commands.test.js`) to cover edge cases and ensure reliability. - Gather feedback from users and stakeholders to identify pain points and desired enhancements, then iterate on the command design and implementation[1][5]. **Example Improvements Table** | Area | Current Issue | Suggested Improvement | |---------------------|-----------------------------|----------------------------------------------| | User Feedback | Minimal or unclear output | Add explicit, contextual messages | | Error Handling | Silent failures | Informative errors and recovery suggestions | | Flexibility | Only single-level movement | Support multi-level and targeted navigation | | Integration | Limited context awareness | Sync with tags, parent/child relationships | | Performance | Slow on large hierarchies | Optimize data access and UI updates | By focusing on these areas, you can make `scope-up` and `scope-down` more robust, user-friendly, and aligned with best practices in scope management and command design[1][5]. --- ## Follow-up 1 **Question:** how might i do this relative to the tag system? **Response:** ## Integrating Scope-Up and Scope-Down with the Tag System **Conceptual Mapping** - In your project, *scope-up* and *scope-down* commands currently operate on the task hierarchy, broadening or narrowing the context by moving between parent and child tasks. - The tag system provides a parallel, orthogonal way to group and filter tasks—tags can represent categories, priorities, or any custom grouping, independent of strict parent/child relationships. **How to Make Scope Commands Tag-Aware** - **Scoping by Tag**: Allow users to "scope-down" into a tag, viewing only tasks associated with that tag. Conversely, "scope-up" could move from a specific tag context back to the broader task list or to a parent tag grouping if you support tag hierarchies. - **Tag-Scoped Views**: When a tag is active as the current scope, all navigation and operations (listing, editing, etc.) are limited to tasks with that tag. This is similar to how some systems let you set the "Tag Scope" to filter navigation and search to only tagged items[1][5]. - **Combining Hierarchy and Tags**: Enable compound scoping, such as "all subtasks of Task X that also have Tag Y." This can be implemented by intersecting the results of hierarchical and tag-based queries. **Implementation Strategies** - **Scope Stack or Context Object**: Maintain a stack or context object that tracks both the current task scope and any active tag filters. Each scope change (up/down or tag in/out) updates this context. - **Commands**: - `scope-down --tag <tag>`: Narrows the view to tasks with the specified tag. - `scope-up --tag`: Removes the tag filter, broadening the view. - `scope-down <subtask-id> --tag <tag>`: Narrows to a specific subtask *and* applies a tag filter. - **Tag Hierarchies (Optional)**: If your tag system supports parent/child tags or tag groups, allow "scope-up" to move from a specific tag to its parent tag or group, and "scope-down" to move into a child tag[4][5]. **Example User Flows** - *Scoping Down by Tag*: User runs `scope-down --tag urgent` → Only tasks tagged "urgent" are shown. - *Combining Task and Tag*: User runs `scope-down <task-id> --tag frontend` → Only subtasks of `<task-id>` that are tagged "frontend" are shown. - *Scoping Up from Tag*: User runs `scope-up --tag` → Removes the tag filter, returning to the previous broader context. **Technical Considerations** - **Efficient Filtering**: Use indexed queries or in-memory filtering to efficiently combine tag and hierarchy filters, especially for large task sets. - **Feedback and UI**: Always display the current scope context (e.g., "Viewing tasks tagged 'urgent' under 'Feature X'") so users understand what they're seeing. - **Extensibility**: Design the scope system so new types of scopes (e.g., by status, assignee) can be added in the future, not just tags and hierarchy. **Relevant Precedents** - Tag-based file systems and management tools often implement "scopes" as dynamic groups defined by tag criteria, allowing users to build up complex views by combining tags and other filters[5]. - Some systems allow scoping into tag management itself, where you can create, edit, or view tags and their associated objects, which could be mirrored in your CLI or UI[4]. **Summary Table** | Command Example | Resulting Scope | |----------------------------------|--------------------------------------------------| | `scope-down --tag bug` | All tasks tagged "bug" | | `scope-down <task-id> --tag api` | Subtasks of `<task-id>` tagged "api" | | `scope-up --tag` | Removes tag filter, returns to previous scope | | `scope-down --tag frontend` | All tasks tagged "frontend" | By integrating tag-based scoping, you make your scope commands more flexible and powerful, supporting both hierarchical and ad-hoc organizational strategies[1][5]. --- *Generated by Task Master Research Command* *Timestamp: 2025-06-14T20:35:03.231Z* ``` -------------------------------------------------------------------------------- /packages/tm-core/src/task-master-core.ts: -------------------------------------------------------------------------------- ```typescript /** * @fileoverview TaskMasterCore facade - main entry point for tm-core functionality */ import { ConfigManager } from './config/config-manager.js'; import { TaskService, type TaskListResult as ListTasksResult, type GetTaskListOptions } from './services/task-service.js'; import { TaskExecutionService, type StartTaskOptions, type StartTaskResult, type ConflictCheckResult } from './services/task-execution-service.js'; import { ERROR_CODES, TaskMasterError } from './errors/task-master-error.js'; import type { IConfiguration } from './interfaces/configuration.interface.js'; import type { Task, TaskStatus, TaskFilter, StorageType } from './types/index.js'; import { ExecutorService, type ExecutorServiceOptions, type ExecutionResult, type ExecutorType } from './executors/index.js'; /** * Options for creating TaskMasterCore instance */ export interface TaskMasterCoreOptions { projectPath: string; configuration?: Partial<IConfiguration>; } /** * Re-export result types from services */ export type { TaskListResult as ListTasksResult } from './services/task-service.js'; export type { GetTaskListOptions } from './services/task-service.js'; export type { StartTaskOptions, StartTaskResult, ConflictCheckResult } from './services/task-execution-service.js'; /** * TaskMasterCore facade class * Provides simplified API for all tm-core operations */ export class TaskMasterCore { private configManager: ConfigManager; private taskService: TaskService; private taskExecutionService: TaskExecutionService; private executorService: ExecutorService | null = null; /** * Create and initialize a new TaskMasterCore instance * This is the ONLY way to create a TaskMasterCore * * @param options - Configuration options for TaskMasterCore * @returns Fully initialized TaskMasterCore instance */ static async create(options: TaskMasterCoreOptions): Promise<TaskMasterCore> { const instance = new TaskMasterCore(); await instance.initialize(options); return instance; } /** * Private constructor - use TaskMasterCore.create() instead * This ensures the TaskMasterCore is always properly initialized */ private constructor() { // Services will be initialized in the initialize() method this.configManager = null as any; this.taskService = null as any; this.taskExecutionService = null as any; } /** * Initialize by loading services * Private - only called by the factory method */ private async initialize(options: TaskMasterCoreOptions): Promise<void> { if (!options.projectPath) { throw new TaskMasterError( 'Project path is required', ERROR_CODES.MISSING_CONFIGURATION ); } try { // Create config manager using factory method this.configManager = await ConfigManager.create(options.projectPath); // Apply configuration overrides if provided if (options.configuration) { await this.configManager.updateConfig(options.configuration); } // Create task service this.taskService = new TaskService(this.configManager); await this.taskService.initialize(); // Create task execution service this.taskExecutionService = new TaskExecutionService(this.taskService); } catch (error) { throw new TaskMasterError( 'Failed to initialize TaskMasterCore', ERROR_CODES.INTERNAL_ERROR, { operation: 'initialize' }, error as Error ); } } /** * Get list of tasks with optional filtering * @deprecated Use getTaskList() instead */ async listTasks(options?: { tag?: string; filter?: TaskFilter; includeSubtasks?: boolean; }): Promise<ListTasksResult> { return this.getTaskList(options); } /** * Get list of tasks with optional filtering */ async getTaskList(options?: GetTaskListOptions): Promise<ListTasksResult> { return this.taskService.getTaskList(options); } /** * Get a specific task by ID */ async getTask(taskId: string, tag?: string): Promise<Task | null> { return this.taskService.getTask(taskId, tag); } /** * Get tasks by status */ async getTasksByStatus( status: TaskStatus | TaskStatus[], tag?: string ): Promise<Task[]> { return this.taskService.getTasksByStatus(status, tag); } /** * Get task statistics */ async getTaskStats(tag?: string): Promise<{ total: number; byStatus: Record<TaskStatus, number>; withSubtasks: number; blocked: number; }> { const stats = await this.taskService.getTaskStats(tag); // Remove storageType from the return to maintain backward compatibility const { storageType, ...restStats } = stats; return restStats; } /** * Get next available task */ async getNextTask(tag?: string): Promise<Task | null> { return this.taskService.getNextTask(tag); } /** * Get current storage type */ getStorageType(): StorageType { return this.taskService.getStorageType(); } /** * Get current active tag */ getActiveTag(): string { return this.configManager.getActiveTag(); } /** * Set active tag */ async setActiveTag(tag: string): Promise<void> { await this.configManager.setActiveTag(tag); } // ==================== Task Execution Methods ==================== /** * Start working on a task with comprehensive business logic */ async startTask( taskId: string, options: StartTaskOptions = {} ): Promise<StartTaskResult> { return this.taskExecutionService.startTask(taskId, options); } /** * Check if a task can be started (no conflicts) */ async canStartTask(taskId: string, force = false): Promise<boolean> { return this.taskExecutionService.canStartTask(taskId, force); } /** * Check for existing in-progress tasks and determine conflicts */ async checkInProgressConflicts( targetTaskId: string ): Promise<ConflictCheckResult> { return this.taskExecutionService.checkInProgressConflicts(targetTaskId); } /** * Get task with subtask resolution */ async getTaskWithSubtask( taskId: string ): Promise<{ task: Task | null; subtask?: any; subtaskId?: string }> { return this.taskExecutionService.getTaskWithSubtask(taskId); } /** * Get the next available task to start */ async getNextAvailableTask(): Promise<string | null> { return this.taskExecutionService.getNextAvailableTask(); } // ==================== Executor Service Methods ==================== /** * Initialize executor service (lazy initialization) */ private getExecutorService(): ExecutorService { if (!this.executorService) { const executorOptions: ExecutorServiceOptions = { projectRoot: this.configManager.getProjectRoot() }; this.executorService = new ExecutorService(executorOptions); } return this.executorService; } /** * Execute a task */ async executeTask( task: Task, executorType?: ExecutorType ): Promise<ExecutionResult> { const executor = this.getExecutorService(); return executor.executeTask(task, executorType); } /** * Stop the current task execution */ async stopCurrentTask(): Promise<void> { if (this.executorService) { await this.executorService.stopCurrentTask(); } } /** * Update task status */ async updateTaskStatus( taskId: string | number, newStatus: TaskStatus, tag?: string ): Promise<{ success: boolean; oldStatus: TaskStatus; newStatus: TaskStatus; taskId: string; }> { return this.taskService.updateTaskStatus(taskId, newStatus, tag); } /** * Close and cleanup resources */ async close(): Promise<void> { // Stop any running executors if (this.executorService) { await this.executorService.stopCurrentTask(); } // TaskService handles storage cleanup internally } } /** * Factory function to create TaskMasterCore instance */ export async function createTaskMasterCore( options: TaskMasterCoreOptions ): Promise<TaskMasterCore> { return TaskMasterCore.create(options); } ``` -------------------------------------------------------------------------------- /packages/tm-core/src/auth/credential-store.ts: -------------------------------------------------------------------------------- ```typescript /** * Credential storage and management */ import fs from 'fs'; import path from 'path'; import { AuthCredentials, AuthenticationError, AuthConfig } from './types.js'; import { getAuthConfig } from './config.js'; import { getLogger } from '../logger/index.js'; /** * CredentialStore manages the persistence and retrieval of authentication credentials. * * Runtime vs Persisted Shape: * - When retrieved (getCredentials): expiresAt is normalized to number (milliseconds since epoch) * - When persisted (saveCredentials): expiresAt is stored as ISO string for readability * * This normalization ensures consistent runtime behavior while maintaining * human-readable persisted format in the auth.json file. */ export class CredentialStore { private static instance: CredentialStore | null = null; private logger = getLogger('CredentialStore'); private config: AuthConfig; // Clock skew tolerance for expiry checks (30 seconds) private readonly CLOCK_SKEW_MS = 30_000; private constructor(config?: Partial<AuthConfig>) { this.config = getAuthConfig(config); } /** * Get the singleton instance of CredentialStore */ static getInstance(config?: Partial<AuthConfig>): CredentialStore { if (!CredentialStore.instance) { CredentialStore.instance = new CredentialStore(config); } else if (config) { // Warn if config is provided after initialization const logger = getLogger('CredentialStore'); logger.warn( 'getInstance called with config after initialization; config is ignored.' ); } return CredentialStore.instance; } /** * Reset the singleton instance (useful for testing) */ static resetInstance(): void { CredentialStore.instance = null; } /** * Get stored authentication credentials * @returns AuthCredentials with expiresAt as number (milliseconds) for runtime use */ getCredentials(options?: { allowExpired?: boolean }): AuthCredentials | null { try { if (!fs.existsSync(this.config.configFile)) { return null; } const authData = JSON.parse( fs.readFileSync(this.config.configFile, 'utf-8') ) as AuthCredentials; // Normalize/migrate timestamps to numeric (handles both number and ISO string) let expiresAtMs: number | undefined; if (typeof authData.expiresAt === 'number') { expiresAtMs = Number.isFinite(authData.expiresAt) ? authData.expiresAt : undefined; } else if (typeof authData.expiresAt === 'string') { const parsed = Date.parse(authData.expiresAt); expiresAtMs = Number.isNaN(parsed) ? undefined : parsed; } else { expiresAtMs = undefined; } // Validate expiration time for tokens if (expiresAtMs === undefined) { this.logger.warn('No valid expiration time provided for token'); return null; } // Update the authData with normalized timestamp authData.expiresAt = expiresAtMs; // Check if the token has expired (with clock skew tolerance) const now = Date.now(); const allowExpired = options?.allowExpired ?? false; if (now >= expiresAtMs - this.CLOCK_SKEW_MS && !allowExpired) { this.logger.warn( 'Authentication token has expired or is about to expire', { expiresAt: authData.expiresAt, currentTime: new Date(now).toISOString(), skewWindow: `${this.CLOCK_SKEW_MS / 1000}s` } ); return null; } // Return valid token return authData; } catch (error) { this.logger.error( `Failed to read auth credentials: ${(error as Error).message}` ); // Quarantine corrupt file to prevent repeated errors try { if (fs.existsSync(this.config.configFile)) { const corruptFile = `${this.config.configFile}.corrupt-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`; fs.renameSync(this.config.configFile, corruptFile); this.logger.warn(`Quarantined corrupt auth file to: ${corruptFile}`); } } catch (quarantineError) { // If we can't quarantine, log but don't throw this.logger.debug( `Could not quarantine corrupt file: ${(quarantineError as Error).message}` ); } return null; } } /** * Save authentication credentials * @param authData - Credentials with expiresAt as number or string (will be persisted as ISO string) */ saveCredentials(authData: AuthCredentials): void { try { // Ensure directory exists if (!fs.existsSync(this.config.configDir)) { fs.mkdirSync(this.config.configDir, { recursive: true, mode: 0o700 }); } // Add timestamp without mutating caller's object authData = { ...authData, savedAt: new Date().toISOString() }; // Validate and normalize expiresAt timestamp if (authData.expiresAt !== undefined) { let validTimestamp: number | undefined; if (typeof authData.expiresAt === 'number') { validTimestamp = Number.isFinite(authData.expiresAt) ? authData.expiresAt : undefined; } else if (typeof authData.expiresAt === 'string') { const parsed = Date.parse(authData.expiresAt); validTimestamp = Number.isNaN(parsed) ? undefined : parsed; } if (validTimestamp === undefined) { throw new AuthenticationError( `Invalid expiresAt format: ${authData.expiresAt}`, 'SAVE_FAILED' ); } // Store as ISO string for consistency authData.expiresAt = new Date(validTimestamp).toISOString(); } // Save credentials atomically with secure permissions const tempFile = `${this.config.configFile}.tmp`; fs.writeFileSync(tempFile, JSON.stringify(authData, null, 2), { mode: 0o600 }); fs.renameSync(tempFile, this.config.configFile); } catch (error) { throw new AuthenticationError( `Failed to save auth credentials: ${(error as Error).message}`, 'SAVE_FAILED', error ); } } /** * Clear stored credentials */ clearCredentials(): void { try { if (fs.existsSync(this.config.configFile)) { fs.unlinkSync(this.config.configFile); } } catch (error) { throw new AuthenticationError( `Failed to clear credentials: ${(error as Error).message}`, 'CLEAR_FAILED', error ); } } /** * Check if credentials exist and are valid */ hasValidCredentials(): boolean { const credentials = this.getCredentials({ allowExpired: false }); return credentials !== null; } /** * Get configuration */ getConfig(): AuthConfig { return { ...this.config }; } /** * Clean up old corrupt auth files * Removes corrupt files older than the specified age */ cleanupCorruptFiles(maxAgeMs: number = 7 * 24 * 60 * 60 * 1000): void { try { const dir = path.dirname(this.config.configFile); const baseName = path.basename(this.config.configFile); const prefix = `${baseName}.corrupt-`; if (!fs.existsSync(dir)) { return; } const entries = fs.readdirSync(dir, { withFileTypes: true }); const now = Date.now(); for (const entry of entries) { if (!entry.isFile()) continue; const file = entry.name; // Check if file matches pattern: baseName.corrupt-{timestamp} if (!file.startsWith(prefix)) continue; const suffix = file.slice(prefix.length); if (!/^\d+$/.test(suffix)) continue; // Fixed regex, not from variable input const filePath = path.join(dir, file); try { const stats = fs.statSync(filePath); const age = now - stats.mtimeMs; if (age > maxAgeMs) { fs.unlinkSync(filePath); this.logger.debug(`Cleaned up old corrupt file: ${file}`); } } catch (error) { // Ignore errors for individual file cleanup this.logger.debug( `Could not clean up corrupt file ${file}: ${(error as Error).message}` ); } } } catch (error) { // Log but don't throw - this is a cleanup operation this.logger.debug( `Error during corrupt file cleanup: ${(error as Error).message}` ); } } } ``` -------------------------------------------------------------------------------- /src/utils/create-mcp-config.js: -------------------------------------------------------------------------------- ```javascript import fs from 'fs'; import path from 'path'; import { log } from '../../scripts/modules/utils.js'; // Return JSON with existing mcp.json formatting style function formatJSONWithTabs(obj) { let json = JSON.stringify(obj, null, '\t'); json = json.replace( /(\[\n\t+)([^[\]]+?)(\n\t+\])/g, (match, openBracket, content, closeBracket) => { // Only convert to single line if content doesn't contain nested objects/arrays if (!content.includes('{') && !content.includes('[')) { const singleLineContent = content .replace(/\n\t+/g, ' ') .replace(/\s+/g, ' ') .trim(); return `[${singleLineContent}]`; } return match; } ); return json; } // Structure matches project conventions (see scripts/init.js) export function setupMCPConfiguration(projectRoot, mcpConfigPath) { // Handle null mcpConfigPath (e.g., for Claude/Codex profiles) if (!mcpConfigPath) { log( 'debug', '[MCP Config] No mcpConfigPath provided, skipping MCP configuration setup' ); return; } // Build the full path to the MCP config file const mcpPath = path.join(projectRoot, mcpConfigPath); const configDir = path.dirname(mcpPath); log('info', `Setting up MCP configuration at ${mcpPath}...`); // New MCP config to be added - references the installed package const newMCPServer = { 'task-master-ai': { command: 'npx', args: ['-y', 'task-master-ai'], env: { ANTHROPIC_API_KEY: 'YOUR_ANTHROPIC_API_KEY_HERE', PERPLEXITY_API_KEY: 'YOUR_PERPLEXITY_API_KEY_HERE', OPENAI_API_KEY: 'YOUR_OPENAI_KEY_HERE', GOOGLE_API_KEY: 'YOUR_GOOGLE_KEY_HERE', XAI_API_KEY: 'YOUR_XAI_KEY_HERE', OPENROUTER_API_KEY: 'YOUR_OPENROUTER_KEY_HERE', MISTRAL_API_KEY: 'YOUR_MISTRAL_KEY_HERE', AZURE_OPENAI_API_KEY: 'YOUR_AZURE_KEY_HERE', OLLAMA_API_KEY: 'YOUR_OLLAMA_API_KEY_HERE' } } }; // Create config directory if it doesn't exist if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } if (fs.existsSync(mcpPath)) { log( 'info', 'MCP configuration file already exists, checking for existing task-master-ai...' ); try { // Read existing config const mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); // Initialize mcpServers if it doesn't exist if (!mcpConfig.mcpServers) { mcpConfig.mcpServers = {}; } // Check if any existing server configuration already has task-master-ai in its args const hasMCPString = Object.values(mcpConfig.mcpServers).some( (server) => server.args && Array.isArray(server.args) && server.args.some( (arg) => typeof arg === 'string' && arg.includes('task-master-ai') ) ); if (hasMCPString) { log( 'info', 'Found existing task-master-ai MCP configuration in mcp.json, leaving untouched' ); return; // Exit early, don't modify the existing configuration } // Add the task-master-ai server if it doesn't exist if (!mcpConfig.mcpServers['task-master-ai']) { mcpConfig.mcpServers['task-master-ai'] = newMCPServer['task-master-ai']; log( 'info', 'Added task-master-ai server to existing MCP configuration' ); } else { log('info', 'task-master-ai server already configured in mcp.json'); } // Write the updated configuration fs.writeFileSync(mcpPath, formatJSONWithTabs(mcpConfig) + '\n'); log('success', 'Updated MCP configuration file'); } catch (error) { log('error', `Failed to update MCP configuration: ${error.message}`); // Create a backup before potentially modifying const backupPath = `${mcpPath}.backup-${Date.now()}`; if (fs.existsSync(mcpPath)) { fs.copyFileSync(mcpPath, backupPath); log('info', `Created backup of existing mcp.json at ${backupPath}`); } // Create new configuration const newMCPConfig = { mcpServers: newMCPServer }; fs.writeFileSync(mcpPath, formatJSONWithTabs(newMCPConfig) + '\n'); log( 'warn', 'Created new MCP configuration file (backup of original file was created if it existed)' ); } } else { // If mcp.json doesn't exist, create it const newMCPConfig = { mcpServers: newMCPServer }; fs.writeFileSync(mcpPath, formatJSONWithTabs(newMCPConfig) + '\n'); log('success', `Created MCP configuration file at ${mcpPath}`); } // Add note to console about MCP integration log('info', 'MCP server will use the installed task-master-ai package'); } /** * Remove Task Master MCP server configuration from an existing mcp.json file * Only removes Task Master entries, preserving other MCP servers * @param {string} projectRoot - Target project directory * @param {string} mcpConfigPath - Relative path to MCP config file (e.g., '.cursor/mcp.json') * @returns {Object} Result object with success status and details */ export function removeTaskMasterMCPConfiguration(projectRoot, mcpConfigPath) { // Handle null mcpConfigPath (e.g., for Claude/Codex profiles) if (!mcpConfigPath) { return { success: true, removed: false, deleted: false, error: null, hasOtherServers: false }; } const mcpPath = path.join(projectRoot, mcpConfigPath); let result = { success: false, removed: false, deleted: false, error: null, hasOtherServers: false }; if (!fs.existsSync(mcpPath)) { result.success = true; result.removed = false; log('debug', `[MCP Config] MCP config file does not exist: ${mcpPath}`); return result; } try { // Read existing config const mcpConfig = JSON.parse(fs.readFileSync(mcpPath, 'utf8')); if (!mcpConfig.mcpServers) { result.success = true; result.removed = false; log('debug', `[MCP Config] No mcpServers section found in: ${mcpPath}`); return result; } // Check if Task Master is configured const hasTaskMaster = mcpConfig.mcpServers['task-master-ai'] || Object.values(mcpConfig.mcpServers).some( (server) => server.args && Array.isArray(server.args) && server.args.some( (arg) => typeof arg === 'string' && arg.includes('task-master-ai') ) ); if (!hasTaskMaster) { result.success = true; result.removed = false; log( 'debug', `[MCP Config] Task Master not found in MCP config: ${mcpPath}` ); return result; } // Remove task-master-ai server delete mcpConfig.mcpServers['task-master-ai']; // Also remove any servers that have task-master-ai in their args Object.keys(mcpConfig.mcpServers).forEach((serverName) => { const server = mcpConfig.mcpServers[serverName]; if ( server.args && Array.isArray(server.args) && server.args.some( (arg) => typeof arg === 'string' && arg.includes('task-master-ai') ) ) { delete mcpConfig.mcpServers[serverName]; log( 'debug', `[MCP Config] Removed server '${serverName}' containing task-master-ai` ); } }); // Check if there are other MCP servers remaining const remainingServers = Object.keys(mcpConfig.mcpServers); result.hasOtherServers = remainingServers.length > 0; if (result.hasOtherServers) { // Write back the modified config with remaining servers fs.writeFileSync(mcpPath, formatJSONWithTabs(mcpConfig) + '\n'); result.success = true; result.removed = true; result.deleted = false; log( 'info', `[MCP Config] Removed Task Master from MCP config, preserving other servers: ${remainingServers.join(', ')}` ); } else { // No other servers, delete the entire file fs.rmSync(mcpPath, { force: true }); result.success = true; result.removed = true; result.deleted = true; log( 'info', `[MCP Config] Removed MCP config file (no other servers remaining): ${mcpPath}` ); } } catch (error) { result.error = error.message; log( 'error', `[MCP Config] Failed to remove Task Master from MCP config: ${error.message}` ); } return result; } ```