This is page 34 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .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
│ │ └── validate-changesets.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
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── 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
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.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_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── 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
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── 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
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── 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
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── 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_CODE_PLUGIN.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
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── 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
│ │ │ ├── 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
│ │ │ ├── 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
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── 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
│ ├── tool-registry.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
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.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
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.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
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.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
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.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
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.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
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── 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
│ │ └── providers
│ │ └── temperature-support.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
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.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
│ │ └── tool-registration.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
│ │ └── prompt-migration.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
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.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
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/apps/cli/src/commands/list.command.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview ListTasks command using Commander's native class pattern
3 | * Extends Commander.Command for better integration with the framework
4 | */
5 |
6 | import {
7 | OUTPUT_FORMATS,
8 | type OutputFormat,
9 | STATUS_ICONS,
10 | TASK_STATUSES,
11 | type Task,
12 | type TaskStatus,
13 | type TmCore,
14 | createTmCore
15 | } from '@tm/core';
16 | import type { StorageType } from '@tm/core';
17 | import chalk from 'chalk';
18 | import { Command } from 'commander';
19 | import {
20 | type NextTaskInfo,
21 | calculateDependencyStatistics,
22 | calculateSubtaskStatistics,
23 | calculateTaskStatistics,
24 | displayDashboards,
25 | displayRecommendedNextTask,
26 | displaySuggestedNextSteps,
27 | getPriorityBreakdown,
28 | getTaskDescription
29 | } from '../ui/index.js';
30 | import { displayCommandHeader } from '../utils/display-helpers.js';
31 | import { displayError } from '../utils/error-handler.js';
32 | import { getProjectRoot } from '../utils/project-root.js';
33 | import { isTaskComplete } from '../utils/task-status.js';
34 | import * as ui from '../utils/ui.js';
35 |
36 | /**
37 | * Options interface for the list command
38 | */
39 | export interface ListCommandOptions {
40 | status?: string;
41 | tag?: string;
42 | withSubtasks?: boolean;
43 | format?: OutputFormat;
44 | json?: boolean;
45 | silent?: boolean;
46 | project?: string;
47 | }
48 |
49 | /**
50 | * Result type from list command
51 | */
52 | export interface ListTasksResult {
53 | tasks: Task[];
54 | total: number;
55 | filtered: number;
56 | tag?: string;
57 | storageType: Exclude<StorageType, 'auto'>;
58 | }
59 |
60 | /**
61 | * ListTasksCommand extending Commander's Command class
62 | * This is a thin presentation layer over @tm/core
63 | */
64 | export class ListTasksCommand extends Command {
65 | private tmCore?: TmCore;
66 | private lastResult?: ListTasksResult;
67 |
68 | constructor(name?: string) {
69 | super(name || 'list');
70 |
71 | // Configure the command
72 | this.description('List tasks with optional filtering')
73 | .alias('ls')
74 | .option('-s, --status <status>', 'Filter by status (comma-separated)')
75 | .option('-t, --tag <tag>', 'Filter by tag')
76 | .option('--with-subtasks', 'Include subtasks in the output')
77 | .option(
78 | '-f, --format <format>',
79 | 'Output format (text, json, compact)',
80 | 'text'
81 | )
82 | .option('--json', 'Output in JSON format (shorthand for --format json)')
83 | .option('--silent', 'Suppress output (useful for programmatic usage)')
84 | .option(
85 | '-p, --project <path>',
86 | 'Project root directory (auto-detected if not provided)'
87 | )
88 | .action(async (options: ListCommandOptions) => {
89 | await this.executeCommand(options);
90 | });
91 | }
92 |
93 | /**
94 | * Execute the list command
95 | */
96 | private async executeCommand(options: ListCommandOptions): Promise<void> {
97 | try {
98 | // Validate options
99 | if (!this.validateOptions(options)) {
100 | process.exit(1);
101 | }
102 |
103 | // Initialize tm-core (project root auto-detected if not provided)
104 | await this.initializeCore(getProjectRoot(options.project));
105 |
106 | // Get tasks from core
107 | const result = await this.getTasks(options);
108 |
109 | // Store result for programmatic access
110 | this.setLastResult(result);
111 |
112 | // Display results
113 | if (!options.silent) {
114 | this.displayResults(result, options);
115 | }
116 | } catch (error: any) {
117 | displayError(error);
118 | }
119 | }
120 |
121 | /**
122 | * Validate command options
123 | */
124 | private validateOptions(options: ListCommandOptions): boolean {
125 | // Validate format
126 | if (
127 | options.format &&
128 | !OUTPUT_FORMATS.includes(options.format as OutputFormat)
129 | ) {
130 | console.error(chalk.red(`Invalid format: ${options.format}`));
131 | console.error(chalk.gray(`Valid formats: ${OUTPUT_FORMATS.join(', ')}`));
132 | return false;
133 | }
134 |
135 | // Validate status
136 | if (options.status) {
137 | const statuses = options.status.split(',').map((s: string) => s.trim());
138 |
139 | for (const status of statuses) {
140 | if (status !== 'all' && !TASK_STATUSES.includes(status as TaskStatus)) {
141 | console.error(chalk.red(`Invalid status: ${status}`));
142 | console.error(
143 | chalk.gray(`Valid statuses: ${TASK_STATUSES.join(', ')}`)
144 | );
145 | return false;
146 | }
147 | }
148 | }
149 |
150 | return true;
151 | }
152 |
153 | /**
154 | * Initialize TmCore
155 | */
156 | private async initializeCore(projectRoot: string): Promise<void> {
157 | if (!this.tmCore) {
158 | this.tmCore = await createTmCore({ projectPath: projectRoot });
159 | }
160 | }
161 |
162 | /**
163 | * Get tasks from tm-core
164 | */
165 | private async getTasks(
166 | options: ListCommandOptions
167 | ): Promise<ListTasksResult> {
168 | if (!this.tmCore) {
169 | throw new Error('TmCore not initialized');
170 | }
171 |
172 | // Build filter
173 | const filter =
174 | options.status && options.status !== 'all'
175 | ? {
176 | status: options.status
177 | .split(',')
178 | .map((s: string) => s.trim() as TaskStatus)
179 | }
180 | : undefined;
181 |
182 | // Call tm-core
183 | const result = await this.tmCore.tasks.list({
184 | tag: options.tag,
185 | filter,
186 | includeSubtasks: options.withSubtasks
187 | });
188 |
189 | return result as ListTasksResult;
190 | }
191 |
192 | /**
193 | * Display results based on format
194 | */
195 | private displayResults(
196 | result: ListTasksResult,
197 | options: ListCommandOptions
198 | ): void {
199 | // If --json flag is set, override format to 'json'
200 | const format = (
201 | options.json ? 'json' : options.format || 'text'
202 | ) as OutputFormat;
203 |
204 | switch (format) {
205 | case 'json':
206 | this.displayJson(result);
207 | break;
208 |
209 | case 'compact':
210 | this.displayCompact(result.tasks, options.withSubtasks);
211 | break;
212 |
213 | case 'text':
214 | default:
215 | this.displayText(result, options.withSubtasks);
216 | break;
217 | }
218 | }
219 |
220 | /**
221 | * Display in JSON format
222 | */
223 | private displayJson(data: ListTasksResult): void {
224 | console.log(
225 | JSON.stringify(
226 | {
227 | tasks: data.tasks,
228 | metadata: {
229 | total: data.total,
230 | filtered: data.filtered,
231 | tag: data.tag,
232 | storageType: data.storageType
233 | }
234 | },
235 | null,
236 | 2
237 | )
238 | );
239 | }
240 |
241 | /**
242 | * Display in compact format
243 | */
244 | private displayCompact(tasks: Task[], withSubtasks?: boolean): void {
245 | tasks.forEach((task) => {
246 | const icon = STATUS_ICONS[task.status];
247 | console.log(`${chalk.cyan(task.id)} ${icon} ${task.title}`);
248 |
249 | if (withSubtasks && task.subtasks?.length) {
250 | task.subtasks.forEach((subtask) => {
251 | const subIcon = STATUS_ICONS[subtask.status];
252 | console.log(
253 | ` ${chalk.gray(String(subtask.id))} ${subIcon} ${chalk.gray(subtask.title)}`
254 | );
255 | });
256 | }
257 | });
258 | }
259 |
260 | /**
261 | * Display in text format with tables
262 | */
263 | private displayText(data: ListTasksResult, withSubtasks?: boolean): void {
264 | const { tasks, tag, storageType } = data;
265 |
266 | // Display header using utility function
267 | displayCommandHeader(this.tmCore, {
268 | tag: tag || 'master',
269 | storageType
270 | });
271 |
272 | // No tasks message
273 | if (tasks.length === 0) {
274 | ui.displayWarning('No tasks found matching the criteria.');
275 | return;
276 | }
277 |
278 | // Calculate statistics
279 | const taskStats = calculateTaskStatistics(tasks);
280 | const subtaskStats = calculateSubtaskStatistics(tasks);
281 | const depStats = calculateDependencyStatistics(tasks);
282 | const priorityBreakdown = getPriorityBreakdown(tasks);
283 |
284 | // Find next task following the same logic as findNextTask
285 | const nextTaskInfo = this.findNextTask(tasks);
286 |
287 | // Get the full task object with complexity data already included
288 | const nextTask = nextTaskInfo
289 | ? tasks.find((t) => String(t.id) === String(nextTaskInfo.id))
290 | : undefined;
291 |
292 | // Display dashboard boxes (nextTask already has complexity from storage enrichment)
293 | displayDashboards(
294 | taskStats,
295 | subtaskStats,
296 | priorityBreakdown,
297 | depStats,
298 | nextTask
299 | );
300 |
301 | // Task table
302 | console.log(
303 | ui.createTaskTable(tasks, {
304 | showSubtasks: withSubtasks,
305 | showDependencies: true,
306 | showComplexity: true // Enable complexity column
307 | })
308 | );
309 |
310 | // Display recommended next task section immediately after table
311 | if (nextTask) {
312 | const description = getTaskDescription(nextTask);
313 |
314 | displayRecommendedNextTask({
315 | id: nextTask.id,
316 | title: nextTask.title,
317 | priority: nextTask.priority,
318 | status: nextTask.status,
319 | dependencies: nextTask.dependencies,
320 | description,
321 | complexity: nextTask.complexity as number | undefined
322 | });
323 | } else {
324 | displayRecommendedNextTask(undefined);
325 | }
326 |
327 | // Display suggested next steps at the end
328 | displaySuggestedNextSteps();
329 | }
330 |
331 | /**
332 | * Set the last result for programmatic access
333 | */
334 | private setLastResult(result: ListTasksResult): void {
335 | this.lastResult = result;
336 | }
337 |
338 | /**
339 | * Find the next task to work on
340 | * Implements the same logic as scripts/modules/task-manager/find-next-task.js
341 | */
342 | private findNextTask(tasks: Task[]): NextTaskInfo | undefined {
343 | const priorityValues: Record<string, number> = {
344 | critical: 4,
345 | high: 3,
346 | medium: 2,
347 | low: 1
348 | };
349 |
350 | // Build set of completed task IDs (including subtasks)
351 | const completedIds = new Set<string>();
352 | tasks.forEach((t) => {
353 | if (isTaskComplete(t.status)) {
354 | completedIds.add(String(t.id));
355 | }
356 | if (t.subtasks) {
357 | t.subtasks.forEach((st) => {
358 | if (isTaskComplete(st.status as TaskStatus)) {
359 | completedIds.add(`${t.id}.${st.id}`);
360 | }
361 | });
362 | }
363 | });
364 |
365 | // First, look for eligible subtasks in in-progress parent tasks
366 | const candidateSubtasks: NextTaskInfo[] = [];
367 |
368 | tasks
369 | .filter(
370 | (t) => t.status === 'in-progress' && t.subtasks && t.subtasks.length > 0
371 | )
372 | .forEach((parent) => {
373 | parent.subtasks!.forEach((st) => {
374 | const stStatus = (st.status || 'pending').toLowerCase();
375 | if (stStatus !== 'pending' && stStatus !== 'in-progress') return;
376 |
377 | // Check if dependencies are satisfied
378 | const fullDeps =
379 | st.dependencies?.map((d) => {
380 | // Handle both numeric and string IDs
381 | if (typeof d === 'string' && d.includes('.')) {
382 | return d;
383 | }
384 | return `${parent.id}.${d}`;
385 | }) ?? [];
386 |
387 | const depsSatisfied =
388 | fullDeps.length === 0 ||
389 | fullDeps.every((depId) => completedIds.has(String(depId)));
390 |
391 | if (depsSatisfied) {
392 | candidateSubtasks.push({
393 | id: `${parent.id}.${st.id}`,
394 | title: st.title || `Subtask ${st.id}`,
395 | priority: st.priority || parent.priority || 'medium',
396 | dependencies: fullDeps.map((d) => String(d))
397 | });
398 | }
399 | });
400 | });
401 |
402 | if (candidateSubtasks.length > 0) {
403 | // Sort by priority, then by dependencies count, then by ID
404 | candidateSubtasks.sort((a, b) => {
405 | const pa = priorityValues[a.priority || 'medium'] ?? 2;
406 | const pb = priorityValues[b.priority || 'medium'] ?? 2;
407 | if (pb !== pa) return pb - pa;
408 |
409 | const depCountA = a.dependencies?.length || 0;
410 | const depCountB = b.dependencies?.length || 0;
411 | if (depCountA !== depCountB) return depCountA - depCountB;
412 |
413 | return String(a.id).localeCompare(String(b.id));
414 | });
415 | return candidateSubtasks[0];
416 | }
417 |
418 | // Fall back to finding eligible top-level tasks
419 | const eligibleTasks = tasks.filter((task) => {
420 | // Skip non-eligible statuses
421 | const status = (task.status || 'pending').toLowerCase();
422 | if (status !== 'pending' && status !== 'in-progress') return false;
423 |
424 | // Check dependencies
425 | const deps = task.dependencies || [];
426 | const depsSatisfied =
427 | deps.length === 0 ||
428 | deps.every((depId) => completedIds.has(String(depId)));
429 |
430 | return depsSatisfied;
431 | });
432 |
433 | if (eligibleTasks.length === 0) return undefined;
434 |
435 | // Sort eligible tasks
436 | eligibleTasks.sort((a, b) => {
437 | // Priority (higher first)
438 | const pa = priorityValues[a.priority || 'medium'] ?? 2;
439 | const pb = priorityValues[b.priority || 'medium'] ?? 2;
440 | if (pb !== pa) return pb - pa;
441 |
442 | // Dependencies count (fewer first)
443 | const depCountA = a.dependencies?.length || 0;
444 | const depCountB = b.dependencies?.length || 0;
445 | if (depCountA !== depCountB) return depCountA - depCountB;
446 |
447 | // ID (lower first)
448 | return Number(a.id) - Number(b.id);
449 | });
450 |
451 | const nextTask = eligibleTasks[0];
452 | return {
453 | id: nextTask.id,
454 | title: nextTask.title,
455 | priority: nextTask.priority,
456 | dependencies: nextTask.dependencies?.map((d) => String(d))
457 | };
458 | }
459 |
460 | /**
461 | * Get the last result (for programmatic usage)
462 | */
463 | getLastResult(): ListTasksResult | undefined {
464 | return this.lastResult;
465 | }
466 |
467 | /**
468 | * Clean up resources
469 | */
470 | async cleanup(): Promise<void> {
471 | if (this.tmCore) {
472 | this.tmCore = undefined;
473 | }
474 | }
475 |
476 | /**
477 | * Register this command on an existing program
478 | */
479 | static register(program: Command, name?: string): ListTasksCommand {
480 | const listCommand = new ListTasksCommand(name);
481 | program.addCommand(listCommand);
482 | return listCommand;
483 | }
484 | }
485 |
```
--------------------------------------------------------------------------------
/apps/cli/tests/integration/commands/autopilot/workflow.test.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Integration tests for autopilot workflow commands
3 | */
4 |
5 | import type { WorkflowState } from '@tm/core';
6 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7 |
8 | // Track file system state in memory - must be in vi.hoisted() for mock access
9 | const {
10 | mockFileSystem,
11 | pathExistsFn,
12 | readJSONFn,
13 | writeJSONFn,
14 | ensureDirFn,
15 | removeFn
16 | } = vi.hoisted(() => {
17 | const mockFileSystem = new Map<string, string>();
18 |
19 | return {
20 | mockFileSystem,
21 | pathExistsFn: vi.fn((path: string) =>
22 | Promise.resolve(mockFileSystem.has(path))
23 | ),
24 | readJSONFn: vi.fn((path: string) => {
25 | const data = mockFileSystem.get(path);
26 | return data
27 | ? Promise.resolve(JSON.parse(data))
28 | : Promise.reject(new Error('File not found'));
29 | }),
30 | writeJSONFn: vi.fn((path: string, data: any) => {
31 | mockFileSystem.set(path, JSON.stringify(data));
32 | return Promise.resolve();
33 | }),
34 | ensureDirFn: vi.fn(() => Promise.resolve()),
35 | removeFn: vi.fn((path: string) => {
36 | mockFileSystem.delete(path);
37 | return Promise.resolve();
38 | })
39 | };
40 | });
41 |
42 | // Mock fs-extra before any imports
43 | vi.mock('fs-extra', () => ({
44 | default: {
45 | pathExists: pathExistsFn,
46 | readJSON: readJSONFn,
47 | writeJSON: writeJSONFn,
48 | ensureDir: ensureDirFn,
49 | remove: removeFn
50 | }
51 | }));
52 |
53 | vi.mock('@tm/core', () => ({
54 | WorkflowOrchestrator: vi.fn().mockImplementation((context) => ({
55 | getCurrentPhase: vi.fn().mockReturnValue('SUBTASK_LOOP'),
56 | getCurrentTDDPhase: vi.fn().mockReturnValue('RED'),
57 | getContext: vi.fn().mockReturnValue(context),
58 | transition: vi.fn(),
59 | restoreState: vi.fn(),
60 | getState: vi.fn().mockReturnValue({ phase: 'SUBTASK_LOOP', context }),
61 | enableAutoPersist: vi.fn(),
62 | canResumeFromState: vi.fn().mockReturnValue(true),
63 | getCurrentSubtask: vi.fn().mockReturnValue({
64 | id: '1',
65 | title: 'Test Subtask',
66 | status: 'pending',
67 | attempts: 0
68 | }),
69 | getProgress: vi.fn().mockReturnValue({
70 | completed: 0,
71 | total: 3,
72 | current: 1,
73 | percentage: 0
74 | }),
75 | canProceed: vi.fn().mockReturnValue(false)
76 | })),
77 | GitAdapter: vi.fn().mockImplementation(() => ({
78 | ensureGitRepository: vi.fn().mockResolvedValue(undefined),
79 | ensureCleanWorkingTree: vi.fn().mockResolvedValue(undefined),
80 | createAndCheckoutBranch: vi.fn().mockResolvedValue(undefined),
81 | hasStagedChanges: vi.fn().mockResolvedValue(true),
82 | getStatus: vi.fn().mockResolvedValue({
83 | staged: ['file1.ts'],
84 | modified: ['file2.ts']
85 | }),
86 | createCommit: vi.fn().mockResolvedValue(undefined),
87 | getLastCommit: vi.fn().mockResolvedValue({
88 | hash: 'abc123def456',
89 | message: 'test commit'
90 | }),
91 | stageFiles: vi.fn().mockResolvedValue(undefined)
92 | })),
93 | CommitMessageGenerator: vi.fn().mockImplementation(() => ({
94 | generateMessage: vi.fn().mockReturnValue('feat: test commit message')
95 | })),
96 | createTaskMasterCore: vi.fn().mockResolvedValue({
97 | getTaskWithSubtask: vi.fn().mockResolvedValue({
98 | task: {
99 | id: '1',
100 | title: 'Test Task',
101 | subtasks: [
102 | { id: '1', title: 'Subtask 1', status: 'pending' },
103 | { id: '2', title: 'Subtask 2', status: 'pending' },
104 | { id: '3', title: 'Subtask 3', status: 'pending' }
105 | ],
106 | tag: 'test'
107 | }
108 | }),
109 | close: vi.fn().mockResolvedValue(undefined)
110 | })
111 | }));
112 |
113 | // Import after mocks are set up
114 | import { Command } from 'commander';
115 | import { AutopilotCommand } from '../../../../src/commands/autopilot/index.js';
116 |
117 | describe('Autopilot Workflow Integration Tests', () => {
118 | const projectRoot = '/test/project';
119 | let program: Command;
120 |
121 | beforeEach(() => {
122 | mockFileSystem.clear();
123 |
124 | // Clear mock call history
125 | pathExistsFn.mockClear();
126 | readJSONFn.mockClear();
127 | writeJSONFn.mockClear();
128 | ensureDirFn.mockClear();
129 | removeFn.mockClear();
130 |
131 | program = new Command();
132 | AutopilotCommand.register(program);
133 |
134 | // Use exitOverride to handle Commander exits in tests
135 | program.exitOverride();
136 | });
137 |
138 | afterEach(() => {
139 | mockFileSystem.clear();
140 | vi.restoreAllMocks();
141 | });
142 |
143 | describe('start command', () => {
144 | it('should initialize workflow and create branch', async () => {
145 | const consoleLogSpy = vi
146 | .spyOn(console, 'log')
147 | .mockImplementation(() => {});
148 |
149 | await program.parseAsync([
150 | 'node',
151 | 'test',
152 | 'autopilot',
153 | 'start',
154 | '1',
155 | '--project-root',
156 | projectRoot,
157 | '--json'
158 | ]);
159 |
160 | // Verify writeJSON was called with state
161 | expect(writeJSONFn).toHaveBeenCalledWith(
162 | expect.stringContaining('workflow-state.json'),
163 | expect.objectContaining({
164 | phase: expect.any(String),
165 | context: expect.any(Object)
166 | }),
167 | expect.any(Object)
168 | );
169 |
170 | consoleLogSpy.mockRestore();
171 | });
172 |
173 | it('should reject invalid task ID', async () => {
174 | const consoleErrorSpy = vi
175 | .spyOn(console, 'error')
176 | .mockImplementation(() => {});
177 |
178 | await expect(
179 | program.parseAsync([
180 | 'node',
181 | 'test',
182 | 'autopilot',
183 | 'start',
184 | 'invalid',
185 | '--project-root',
186 | projectRoot,
187 | '--json'
188 | ])
189 | ).rejects.toMatchObject({ exitCode: 1 });
190 |
191 | consoleErrorSpy.mockRestore();
192 | });
193 |
194 | it('should reject starting when workflow exists without force', async () => {
195 | // Create existing state
196 | const mockState: WorkflowState = {
197 | phase: 'SUBTASK_LOOP',
198 | context: {
199 | taskId: '1',
200 | subtasks: [],
201 | currentSubtaskIndex: 0,
202 | errors: [],
203 | metadata: {}
204 | }
205 | };
206 |
207 | mockFileSystem.set(
208 | `${projectRoot}/.taskmaster/workflow-state.json`,
209 | JSON.stringify(mockState)
210 | );
211 |
212 | const consoleErrorSpy = vi
213 | .spyOn(console, 'error')
214 | .mockImplementation(() => {});
215 |
216 | await expect(
217 | program.parseAsync([
218 | 'node',
219 | 'test',
220 | 'autopilot',
221 | 'start',
222 | '1',
223 | '--project-root',
224 | projectRoot,
225 | '--json'
226 | ])
227 | ).rejects.toMatchObject({ exitCode: 1 });
228 |
229 | consoleErrorSpy.mockRestore();
230 | });
231 | });
232 |
233 | describe('resume command', () => {
234 | beforeEach(() => {
235 | // Create saved state
236 | const mockState: WorkflowState = {
237 | phase: 'SUBTASK_LOOP',
238 | context: {
239 | taskId: '1',
240 | subtasks: [
241 | {
242 | id: '1',
243 | title: 'Test Subtask',
244 | status: 'pending',
245 | attempts: 0
246 | }
247 | ],
248 | currentSubtaskIndex: 0,
249 | currentTDDPhase: 'RED',
250 | branchName: 'task-1',
251 | errors: [],
252 | metadata: {}
253 | }
254 | };
255 |
256 | mockFileSystem.set(
257 | `${projectRoot}/.taskmaster/workflow-state.json`,
258 | JSON.stringify(mockState)
259 | );
260 | });
261 |
262 | it('should restore workflow from saved state', async () => {
263 | const consoleLogSpy = vi
264 | .spyOn(console, 'log')
265 | .mockImplementation(() => {});
266 |
267 | await program.parseAsync([
268 | 'node',
269 | 'test',
270 | 'autopilot',
271 | 'resume',
272 | '--project-root',
273 | projectRoot,
274 | '--json'
275 | ]);
276 |
277 | expect(consoleLogSpy).toHaveBeenCalled();
278 | const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
279 | expect(output.success).toBe(true);
280 | expect(output.taskId).toBe('1');
281 |
282 | consoleLogSpy.mockRestore();
283 | });
284 |
285 | it('should error when no state exists', async () => {
286 | mockFileSystem.clear();
287 |
288 | const consoleErrorSpy = vi
289 | .spyOn(console, 'error')
290 | .mockImplementation(() => {});
291 |
292 | await expect(
293 | program.parseAsync([
294 | 'node',
295 | 'test',
296 | 'autopilot',
297 | 'resume',
298 | '--project-root',
299 | projectRoot,
300 | '--json'
301 | ])
302 | ).rejects.toMatchObject({ exitCode: 1 });
303 |
304 | consoleErrorSpy.mockRestore();
305 | });
306 | });
307 |
308 | describe('next command', () => {
309 | beforeEach(() => {
310 | const mockState: WorkflowState = {
311 | phase: 'SUBTASK_LOOP',
312 | context: {
313 | taskId: '1',
314 | subtasks: [
315 | {
316 | id: '1',
317 | title: 'Test Subtask',
318 | status: 'pending',
319 | attempts: 0
320 | }
321 | ],
322 | currentSubtaskIndex: 0,
323 | currentTDDPhase: 'RED',
324 | branchName: 'task-1',
325 | errors: [],
326 | metadata: {}
327 | }
328 | };
329 |
330 | mockFileSystem.set(
331 | `${projectRoot}/.taskmaster/workflow-state.json`,
332 | JSON.stringify(mockState)
333 | );
334 | });
335 |
336 | it('should return next action in JSON format', async () => {
337 | const consoleLogSpy = vi
338 | .spyOn(console, 'log')
339 | .mockImplementation(() => {});
340 |
341 | await program.parseAsync([
342 | 'node',
343 | 'test',
344 | 'autopilot',
345 | 'next',
346 | '--project-root',
347 | projectRoot,
348 | '--json'
349 | ]);
350 |
351 | expect(consoleLogSpy).toHaveBeenCalled();
352 | const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
353 | expect(output.action).toBe('generate_test');
354 | expect(output.phase).toBe('SUBTASK_LOOP');
355 | expect(output.tddPhase).toBe('RED');
356 |
357 | consoleLogSpy.mockRestore();
358 | });
359 | });
360 |
361 | describe('status command', () => {
362 | beforeEach(() => {
363 | const mockState: WorkflowState = {
364 | phase: 'SUBTASK_LOOP',
365 | context: {
366 | taskId: '1',
367 | subtasks: [
368 | { id: '1', title: 'Subtask 1', status: 'completed', attempts: 1 },
369 | { id: '2', title: 'Subtask 2', status: 'pending', attempts: 0 },
370 | { id: '3', title: 'Subtask 3', status: 'pending', attempts: 0 }
371 | ],
372 | currentSubtaskIndex: 1,
373 | currentTDDPhase: 'RED',
374 | branchName: 'task-1',
375 | errors: [],
376 | metadata: {}
377 | }
378 | };
379 |
380 | mockFileSystem.set(
381 | `${projectRoot}/.taskmaster/workflow-state.json`,
382 | JSON.stringify(mockState)
383 | );
384 | });
385 |
386 | it('should display workflow progress', async () => {
387 | const consoleLogSpy = vi
388 | .spyOn(console, 'log')
389 | .mockImplementation(() => {});
390 |
391 | await program.parseAsync([
392 | 'node',
393 | 'test',
394 | 'autopilot',
395 | 'status',
396 | '--project-root',
397 | projectRoot,
398 | '--json'
399 | ]);
400 |
401 | expect(consoleLogSpy).toHaveBeenCalled();
402 | const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
403 | expect(output.taskId).toBe('1');
404 | expect(output.phase).toBe('SUBTASK_LOOP');
405 | expect(output.progress).toBeDefined();
406 | expect(output.subtasks).toHaveLength(3);
407 |
408 | consoleLogSpy.mockRestore();
409 | });
410 | });
411 |
412 | describe('complete command', () => {
413 | beforeEach(() => {
414 | const mockState: WorkflowState = {
415 | phase: 'SUBTASK_LOOP',
416 | context: {
417 | taskId: '1',
418 | subtasks: [
419 | {
420 | id: '1',
421 | title: 'Test Subtask',
422 | status: 'in-progress',
423 | attempts: 0
424 | }
425 | ],
426 | currentSubtaskIndex: 0,
427 | currentTDDPhase: 'RED',
428 | branchName: 'task-1',
429 | errors: [],
430 | metadata: {}
431 | }
432 | };
433 |
434 | mockFileSystem.set(
435 | `${projectRoot}/.taskmaster/workflow-state.json`,
436 | JSON.stringify(mockState)
437 | );
438 | });
439 |
440 | it('should validate RED phase has failures', async () => {
441 | const consoleErrorSpy = vi
442 | .spyOn(console, 'error')
443 | .mockImplementation(() => {});
444 |
445 | await expect(
446 | program.parseAsync([
447 | 'node',
448 | 'test',
449 | 'autopilot',
450 | 'complete',
451 | '--project-root',
452 | projectRoot,
453 | '--results',
454 | '{"total":10,"passed":10,"failed":0,"skipped":0}',
455 | '--json'
456 | ])
457 | ).rejects.toMatchObject({ exitCode: 1 });
458 |
459 | consoleErrorSpy.mockRestore();
460 | });
461 |
462 | it('should complete RED phase with failures', async () => {
463 | const consoleLogSpy = vi
464 | .spyOn(console, 'log')
465 | .mockImplementation(() => {});
466 |
467 | await program.parseAsync([
468 | 'node',
469 | 'test',
470 | 'autopilot',
471 | 'complete',
472 | '--project-root',
473 | projectRoot,
474 | '--results',
475 | '{"total":10,"passed":9,"failed":1,"skipped":0}',
476 | '--json'
477 | ]);
478 |
479 | expect(consoleLogSpy).toHaveBeenCalled();
480 | const output = JSON.parse(consoleLogSpy.mock.calls[0][0]);
481 | expect(output.success).toBe(true);
482 | expect(output.nextPhase).toBe('GREEN');
483 |
484 | consoleLogSpy.mockRestore();
485 | });
486 | });
487 |
488 | describe('abort command', () => {
489 | beforeEach(() => {
490 | const mockState: WorkflowState = {
491 | phase: 'SUBTASK_LOOP',
492 | context: {
493 | taskId: '1',
494 | subtasks: [
495 | {
496 | id: '1',
497 | title: 'Test Subtask',
498 | status: 'pending',
499 | attempts: 0
500 | }
501 | ],
502 | currentSubtaskIndex: 0,
503 | currentTDDPhase: 'RED',
504 | branchName: 'task-1',
505 | errors: [],
506 | metadata: {}
507 | }
508 | };
509 |
510 | mockFileSystem.set(
511 | `${projectRoot}/.taskmaster/workflow-state.json`,
512 | JSON.stringify(mockState)
513 | );
514 | });
515 |
516 | it('should abort workflow and delete state', async () => {
517 | const consoleLogSpy = vi
518 | .spyOn(console, 'log')
519 | .mockImplementation(() => {});
520 |
521 | await program.parseAsync([
522 | 'node',
523 | 'test',
524 | 'autopilot',
525 | 'abort',
526 | '--project-root',
527 | projectRoot,
528 | '--force',
529 | '--json'
530 | ]);
531 |
532 | // Verify remove was called
533 | expect(removeFn).toHaveBeenCalledWith(
534 | expect.stringContaining('workflow-state.json')
535 | );
536 |
537 | consoleLogSpy.mockRestore();
538 | });
539 | });
540 | });
541 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/add-task.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Tests for the add-task.js module
3 | */
4 | import { jest } from '@jest/globals';
5 | import { hasCodebaseAnalysis } from '../../../../../scripts/modules/config-manager.js';
6 |
7 | // Mock the dependencies before importing the module under test
8 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
9 | readJSON: jest.fn(),
10 | writeJSON: jest.fn(),
11 | log: jest.fn(),
12 | CONFIG: {
13 | model: 'mock-claude-model',
14 | maxTokens: 4000,
15 | temperature: 0.7,
16 | debug: false
17 | },
18 | sanitizePrompt: jest.fn((prompt) => prompt),
19 | truncate: jest.fn((text) => text),
20 | isSilentMode: jest.fn(() => false),
21 | findTaskById: jest.fn((tasks, id) => {
22 | if (!tasks) return null;
23 | const allTasks = [];
24 | const queue = [...tasks];
25 | while (queue.length > 0) {
26 | const task = queue.shift();
27 | allTasks.push(task);
28 | if (task.subtasks) {
29 | queue.push(...task.subtasks);
30 | }
31 | }
32 | return allTasks.find((task) => String(task.id) === String(id));
33 | }),
34 | getCurrentTag: jest.fn(() => 'master'),
35 | ensureTagMetadata: jest.fn((tagObj) => tagObj),
36 | flattenTasksWithSubtasks: jest.fn((tasks) => {
37 | const allTasks = [];
38 | const queue = [...(tasks || [])];
39 | while (queue.length > 0) {
40 | const task = queue.shift();
41 | allTasks.push(task);
42 | if (task.subtasks) {
43 | for (const subtask of task.subtasks) {
44 | queue.push({ ...subtask, id: `${task.id}.${subtask.id}` });
45 | }
46 | }
47 | }
48 | return allTasks;
49 | }),
50 | markMigrationForNotice: jest.fn(),
51 | performCompleteTagMigration: jest.fn(),
52 | setTasksForTag: jest.fn(),
53 | getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || [])
54 | }));
55 |
56 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
57 | displayBanner: jest.fn(),
58 | getStatusWithColor: jest.fn((status) => status),
59 | startLoadingIndicator: jest.fn(),
60 | stopLoadingIndicator: jest.fn(),
61 | succeedLoadingIndicator: jest.fn(),
62 | failLoadingIndicator: jest.fn(),
63 | warnLoadingIndicator: jest.fn(),
64 | infoLoadingIndicator: jest.fn(),
65 | displayAiUsageSummary: jest.fn(),
66 | displayContextAnalysis: jest.fn()
67 | }));
68 |
69 | jest.unstable_mockModule(
70 | '../../../../../scripts/modules/ai-services-unified.js',
71 | () => ({
72 | generateObjectService: jest.fn().mockResolvedValue({
73 | mainResult: {
74 | object: {
75 | title: 'Task from prompt: Create a new authentication system',
76 | description:
77 | 'Task generated from: Create a new authentication system',
78 | details:
79 | 'Implementation details for task generated from prompt: Create a new authentication system',
80 | testStrategy: 'Write unit tests to verify functionality',
81 | dependencies: []
82 | }
83 | },
84 | telemetryData: {
85 | timestamp: new Date().toISOString(),
86 | userId: '1234567890',
87 | commandName: 'add-task',
88 | modelUsed: 'claude-3-5-sonnet',
89 | providerName: 'anthropic',
90 | inputTokens: 1000,
91 | outputTokens: 500,
92 | totalTokens: 1500,
93 | totalCost: 0.012414,
94 | currency: 'USD'
95 | }
96 | })
97 | })
98 | );
99 |
100 | jest.unstable_mockModule(
101 | '../../../../../scripts/modules/config-manager.js',
102 | () => ({
103 | getDefaultPriority: jest.fn(() => 'medium'),
104 | hasCodebaseAnalysis: jest.fn(() => false)
105 | })
106 | );
107 |
108 | jest.unstable_mockModule(
109 | '../../../../../scripts/modules/utils/contextGatherer.js',
110 | () => ({
111 | default: jest.fn().mockImplementation(() => ({
112 | gather: jest.fn().mockResolvedValue({
113 | contextSummary: 'Mock context summary',
114 | allRelatedTaskIds: [],
115 | graphVisualization: 'Mock graph'
116 | })
117 | }))
118 | })
119 | );
120 |
121 | jest.unstable_mockModule(
122 | '../../../../../scripts/modules/task-manager/generate-task-files.js',
123 | () => ({
124 | default: jest.fn().mockResolvedValue()
125 | })
126 | );
127 |
128 | jest.unstable_mockModule(
129 | '../../../../../scripts/modules/prompt-manager.js',
130 | () => ({
131 | getPromptManager: jest.fn().mockReturnValue({
132 | loadPrompt: jest.fn().mockResolvedValue({
133 | systemPrompt: 'Mocked system prompt',
134 | userPrompt: 'Mocked user prompt'
135 | })
136 | })
137 | })
138 | );
139 |
140 | // Mock external UI libraries
141 | jest.unstable_mockModule('chalk', () => ({
142 | default: {
143 | white: { bold: jest.fn((text) => text) },
144 | cyan: Object.assign(
145 | jest.fn((text) => text),
146 | {
147 | bold: jest.fn((text) => text)
148 | }
149 | ),
150 | green: jest.fn((text) => text),
151 | yellow: jest.fn((text) => text),
152 | bold: jest.fn((text) => text)
153 | }
154 | }));
155 |
156 | jest.unstable_mockModule('boxen', () => ({
157 | default: jest.fn((text) => text)
158 | }));
159 |
160 | jest.unstable_mockModule('cli-table3', () => ({
161 | default: jest.fn().mockImplementation(() => ({
162 | push: jest.fn(),
163 | toString: jest.fn(() => 'mocked table')
164 | }))
165 | }));
166 |
167 | // Import the mocked modules
168 | const { readJSON, writeJSON, log } = await import(
169 | '../../../../../scripts/modules/utils.js'
170 | );
171 |
172 | const { generateObjectService } = await import(
173 | '../../../../../scripts/modules/ai-services-unified.js'
174 | );
175 |
176 | const generateTaskFiles = (
177 | await import(
178 | '../../../../../scripts/modules/task-manager/generate-task-files.js'
179 | )
180 | ).default;
181 |
182 | // Import the module under test
183 | const { default: addTask } = await import(
184 | '../../../../../scripts/modules/task-manager/add-task.js'
185 | );
186 |
187 | describe('addTask', () => {
188 | const sampleTasks = {
189 | master: {
190 | tasks: [
191 | {
192 | id: 1,
193 | title: 'Task 1',
194 | description: 'First task',
195 | status: 'pending',
196 | dependencies: []
197 | },
198 | {
199 | id: 2,
200 | title: 'Task 2',
201 | description: 'Second task',
202 | status: 'pending',
203 | dependencies: []
204 | },
205 | {
206 | id: 3,
207 | title: 'Task 3',
208 | description: 'Third task',
209 | status: 'pending',
210 | dependencies: [1]
211 | }
212 | ]
213 | }
214 | };
215 |
216 | // Create a helper function for consistent mcpLog mock
217 | const createMcpLogMock = () => ({
218 | info: jest.fn(),
219 | warn: jest.fn(),
220 | error: jest.fn(),
221 | debug: jest.fn(),
222 | success: jest.fn()
223 | });
224 |
225 | beforeEach(() => {
226 | jest.clearAllMocks();
227 | readJSON.mockReturnValue(JSON.parse(JSON.stringify(sampleTasks)));
228 |
229 | // Mock console.log to avoid output during tests
230 | jest.spyOn(console, 'log').mockImplementation(() => {});
231 | });
232 |
233 | afterEach(() => {
234 | console.log.mockRestore();
235 | });
236 |
237 | test('should add a new task using AI', async () => {
238 | // Arrange
239 | const prompt = 'Create a new authentication system';
240 | const context = {
241 | mcpLog: createMcpLogMock(),
242 | projectRoot: '/mock/project/root',
243 | tag: 'master'
244 | };
245 |
246 | // Act
247 | const result = await addTask(
248 | 'tasks/tasks.json',
249 | prompt,
250 | [],
251 | 'medium',
252 | context,
253 | 'json'
254 | );
255 |
256 | // Assert
257 | expect(readJSON).toHaveBeenCalledWith(
258 | 'tasks/tasks.json',
259 | '/mock/project/root',
260 | 'master'
261 | );
262 | expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
263 | expect(writeJSON).toHaveBeenCalledWith(
264 | 'tasks/tasks.json',
265 | expect.objectContaining({
266 | master: expect.objectContaining({
267 | tasks: expect.arrayContaining([
268 | expect.objectContaining({
269 | id: 4, // Next ID after existing tasks
270 | title: expect.stringContaining(
271 | 'Create a new authentication system'
272 | ),
273 | status: 'pending'
274 | })
275 | ])
276 | })
277 | }),
278 | '/mock/project/root', // projectRoot parameter
279 | 'master' // tag parameter
280 | );
281 | expect(result).toEqual(
282 | expect.objectContaining({
283 | newTaskId: 4,
284 | telemetryData: expect.any(Object)
285 | })
286 | );
287 | });
288 |
289 | test('should validate dependencies when adding a task', async () => {
290 | // Arrange
291 | const prompt = 'Create a new authentication system';
292 | const validDependencies = [1, 2]; // These exist in sampleTasks
293 | const context = {
294 | mcpLog: createMcpLogMock(),
295 | projectRoot: '/mock/project/root',
296 | tag: 'master'
297 | };
298 |
299 | // Act
300 | const result = await addTask(
301 | 'tasks/tasks.json',
302 | prompt,
303 | validDependencies,
304 | 'medium',
305 | context,
306 | 'json'
307 | );
308 |
309 | // Assert
310 | expect(writeJSON).toHaveBeenCalledWith(
311 | 'tasks/tasks.json',
312 | expect.objectContaining({
313 | master: expect.objectContaining({
314 | tasks: expect.arrayContaining([
315 | expect.objectContaining({
316 | id: 4,
317 | dependencies: validDependencies
318 | })
319 | ])
320 | })
321 | }),
322 | '/mock/project/root', // projectRoot parameter
323 | 'master' // tag parameter
324 | );
325 | });
326 |
327 | test('should filter out invalid dependencies', async () => {
328 | // Arrange
329 | const prompt = 'Create a new authentication system';
330 | const invalidDependencies = [999]; // Non-existent task ID
331 | const context = {
332 | mcpLog: createMcpLogMock(),
333 | projectRoot: '/mock/project/root',
334 | tag: 'master'
335 | };
336 |
337 | // Act
338 | const result = await addTask(
339 | 'tasks/tasks.json',
340 | prompt,
341 | invalidDependencies,
342 | 'medium',
343 | context,
344 | 'json'
345 | );
346 |
347 | // Assert
348 | expect(writeJSON).toHaveBeenCalledWith(
349 | 'tasks/tasks.json',
350 | expect.objectContaining({
351 | master: expect.objectContaining({
352 | tasks: expect.arrayContaining([
353 | expect.objectContaining({
354 | id: 4,
355 | dependencies: [] // Invalid dependencies should be filtered out
356 | })
357 | ])
358 | })
359 | }),
360 | '/mock/project/root', // projectRoot parameter
361 | 'master' // tag parameter
362 | );
363 | expect(context.mcpLog.warn).toHaveBeenCalledWith(
364 | expect.stringContaining(
365 | 'The following dependencies do not exist or are invalid: 999'
366 | )
367 | );
368 | });
369 |
370 | test('should use specified priority', async () => {
371 | // Arrange
372 | const prompt = 'Create a new authentication system';
373 | const priority = 'high';
374 | const context = {
375 | mcpLog: createMcpLogMock(),
376 | projectRoot: '/mock/project/root',
377 | tag: 'master'
378 | };
379 |
380 | // Act
381 | await addTask('tasks/tasks.json', prompt, [], priority, context, 'json');
382 |
383 | // Assert
384 | expect(writeJSON).toHaveBeenCalledWith(
385 | 'tasks/tasks.json',
386 | expect.objectContaining({
387 | master: expect.objectContaining({
388 | tasks: expect.arrayContaining([
389 | expect.objectContaining({
390 | priority: priority
391 | })
392 | ])
393 | })
394 | }),
395 | '/mock/project/root', // projectRoot parameter
396 | 'master' // tag parameter
397 | );
398 | });
399 |
400 | test('should handle empty tasks file', async () => {
401 | // Arrange
402 | readJSON.mockReturnValue({ master: { tasks: [] } });
403 | const prompt = 'Create a new authentication system';
404 | const context = {
405 | mcpLog: createMcpLogMock(),
406 | projectRoot: '/mock/project/root',
407 | tag: 'master'
408 | };
409 |
410 | // Act
411 | const result = await addTask(
412 | 'tasks/tasks.json',
413 | prompt,
414 | [],
415 | 'medium',
416 | context,
417 | 'json'
418 | );
419 |
420 | // Assert
421 | expect(result.newTaskId).toBe(1); // First task should have ID 1
422 | expect(writeJSON).toHaveBeenCalledWith(
423 | 'tasks/tasks.json',
424 | expect.objectContaining({
425 | master: expect.objectContaining({
426 | tasks: expect.arrayContaining([
427 | expect.objectContaining({
428 | id: 1
429 | })
430 | ])
431 | })
432 | }),
433 | '/mock/project/root', // projectRoot parameter
434 | 'master' // tag parameter
435 | );
436 | });
437 |
438 | test('should handle missing tasks file', async () => {
439 | // Arrange
440 | readJSON.mockReturnValue(null);
441 | const prompt = 'Create a new authentication system';
442 | const context = {
443 | mcpLog: createMcpLogMock(),
444 | projectRoot: '/mock/project/root',
445 | tag: 'master'
446 | };
447 |
448 | // Act
449 | const result = await addTask(
450 | 'tasks/tasks.json',
451 | prompt,
452 | [],
453 | 'medium',
454 | context,
455 | 'json'
456 | );
457 |
458 | // Assert
459 | expect(result.newTaskId).toBe(1); // First task should have ID 1
460 | expect(writeJSON).toHaveBeenCalledTimes(1); // Should create file and add task in one go.
461 | });
462 |
463 | test('should handle AI service errors', async () => {
464 | // Arrange
465 | generateObjectService.mockRejectedValueOnce(new Error('AI service failed'));
466 | const prompt = 'Create a new authentication system';
467 | const context = {
468 | mcpLog: createMcpLogMock(),
469 | projectRoot: '/mock/project/root',
470 | tag: 'master'
471 | };
472 |
473 | // Act & Assert
474 | await expect(
475 | addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json')
476 | ).rejects.toThrow('AI service failed');
477 | });
478 |
479 | test('should handle file read errors', async () => {
480 | // Arrange
481 | readJSON.mockImplementation(() => {
482 | throw new Error('File read failed');
483 | });
484 | const prompt = 'Create a new authentication system';
485 | const context = {
486 | mcpLog: createMcpLogMock(),
487 | projectRoot: '/mock/project/root',
488 | tag: 'master'
489 | };
490 |
491 | // Act & Assert
492 | await expect(
493 | addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json')
494 | ).rejects.toThrow('File read failed');
495 | });
496 |
497 | test('should handle file write errors', async () => {
498 | // Arrange
499 | writeJSON.mockImplementation(() => {
500 | throw new Error('File write failed');
501 | });
502 | const prompt = 'Create a new authentication system';
503 | const context = {
504 | mcpLog: createMcpLogMock(),
505 | projectRoot: '/mock/project/root',
506 | tag: 'master'
507 | };
508 |
509 | // Act & Assert
510 | await expect(
511 | addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json')
512 | ).rejects.toThrow('File write failed');
513 | });
514 | });
515 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/dependency-manager/cross-tag-dependencies.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 | import {
3 | validateCrossTagMove,
4 | findCrossTagDependencies,
5 | getDependentTaskIds,
6 | validateSubtaskMove,
7 | canMoveWithDependencies
8 | } from '../../../../../scripts/modules/dependency-manager.js';
9 |
10 | describe('Cross-Tag Dependency Validation', () => {
11 | describe('validateCrossTagMove', () => {
12 | const mockAllTasks = [
13 | { id: 1, tag: 'backlog', dependencies: [2], title: 'Task 1' },
14 | { id: 2, tag: 'backlog', dependencies: [], title: 'Task 2' },
15 | { id: 3, tag: 'in-progress', dependencies: [1], title: 'Task 3' },
16 | { id: 4, tag: 'done', dependencies: [], title: 'Task 4' }
17 | ];
18 |
19 | it('should allow move when no dependencies exist', () => {
20 | const task = { id: 2, dependencies: [], title: 'Task 2' };
21 | const result = validateCrossTagMove(
22 | task,
23 | 'backlog',
24 | 'in-progress',
25 | mockAllTasks
26 | );
27 |
28 | expect(result.canMove).toBe(true);
29 | expect(result.conflicts).toHaveLength(0);
30 | });
31 |
32 | it('should block move when cross-tag dependencies exist', () => {
33 | const task = { id: 1, dependencies: [2], title: 'Task 1' };
34 | const result = validateCrossTagMove(
35 | task,
36 | 'backlog',
37 | 'in-progress',
38 | mockAllTasks
39 | );
40 |
41 | expect(result.canMove).toBe(false);
42 | expect(result.conflicts).toHaveLength(1);
43 | expect(result.conflicts[0]).toMatchObject({
44 | taskId: 1,
45 | dependencyId: 2,
46 | dependencyTag: 'backlog'
47 | });
48 | });
49 |
50 | it('should allow move when dependencies are in target tag', () => {
51 | const task = { id: 3, dependencies: [1], title: 'Task 3' };
52 | // Move both task 1 and task 3 to in-progress, then move task 1 to done
53 | const updatedTasks = mockAllTasks.map((t) => {
54 | if (t.id === 1) return { ...t, tag: 'in-progress' };
55 | if (t.id === 3) return { ...t, tag: 'in-progress' };
56 | return t;
57 | });
58 | // Now move task 1 to done
59 | const updatedTasks2 = updatedTasks.map((t) =>
60 | t.id === 1 ? { ...t, tag: 'done' } : t
61 | );
62 | const result = validateCrossTagMove(
63 | task,
64 | 'in-progress',
65 | 'done',
66 | updatedTasks2
67 | );
68 |
69 | expect(result.canMove).toBe(true);
70 | expect(result.conflicts).toHaveLength(0);
71 | });
72 |
73 | it('should handle multiple dependencies correctly', () => {
74 | const task = { id: 5, dependencies: [1, 3], title: 'Task 5' };
75 | const result = validateCrossTagMove(
76 | task,
77 | 'backlog',
78 | 'done',
79 | mockAllTasks
80 | );
81 |
82 | expect(result.canMove).toBe(false);
83 | expect(result.conflicts).toHaveLength(2);
84 | expect(result.conflicts[0].dependencyId).toBe(1);
85 | expect(result.conflicts[1].dependencyId).toBe(3);
86 | });
87 |
88 | it('should throw error for invalid task parameter', () => {
89 | expect(() =>
90 | validateCrossTagMove(null, 'backlog', 'in-progress', mockAllTasks)
91 | ).toThrow('Task parameter must be a valid object');
92 | });
93 |
94 | it('should throw error for invalid source tag', () => {
95 | const task = { id: 1, dependencies: [], title: 'Task 1' };
96 | expect(() =>
97 | validateCrossTagMove(task, '', 'in-progress', mockAllTasks)
98 | ).toThrow('Source tag must be a valid string');
99 | });
100 |
101 | it('should throw error for invalid target tag', () => {
102 | const task = { id: 1, dependencies: [], title: 'Task 1' };
103 | expect(() =>
104 | validateCrossTagMove(task, 'backlog', null, mockAllTasks)
105 | ).toThrow('Target tag must be a valid string');
106 | });
107 |
108 | it('should throw error for invalid allTasks parameter', () => {
109 | const task = { id: 1, dependencies: [], title: 'Task 1' };
110 | expect(() =>
111 | validateCrossTagMove(task, 'backlog', 'in-progress', 'not-an-array')
112 | ).toThrow('All tasks parameter must be an array');
113 | });
114 | });
115 |
116 | describe('findCrossTagDependencies', () => {
117 | const mockAllTasks = [
118 | { id: 1, tag: 'backlog', dependencies: [2], title: 'Task 1' },
119 | { id: 2, tag: 'backlog', dependencies: [], title: 'Task 2' },
120 | { id: 3, tag: 'in-progress', dependencies: [1], title: 'Task 3' },
121 | { id: 4, tag: 'done', dependencies: [], title: 'Task 4' }
122 | ];
123 |
124 | it('should find cross-tag dependencies for multiple tasks', () => {
125 | const sourceTasks = [
126 | { id: 1, dependencies: [2], title: 'Task 1' },
127 | { id: 3, dependencies: [1], title: 'Task 3' }
128 | ];
129 | const conflicts = findCrossTagDependencies(
130 | sourceTasks,
131 | 'backlog',
132 | 'done',
133 | mockAllTasks
134 | );
135 |
136 | expect(conflicts).toHaveLength(2);
137 | expect(conflicts[0].taskId).toBe(1);
138 | expect(conflicts[0].dependencyId).toBe(2);
139 | expect(conflicts[1].taskId).toBe(3);
140 | expect(conflicts[1].dependencyId).toBe(1);
141 | });
142 |
143 | it('should return empty array when no cross-tag dependencies exist', () => {
144 | const sourceTasks = [
145 | { id: 2, dependencies: [], title: 'Task 2' },
146 | { id: 4, dependencies: [], title: 'Task 4' }
147 | ];
148 | const conflicts = findCrossTagDependencies(
149 | sourceTasks,
150 | 'backlog',
151 | 'done',
152 | mockAllTasks
153 | );
154 |
155 | expect(conflicts).toHaveLength(0);
156 | });
157 |
158 | it('should handle tasks without dependencies', () => {
159 | const sourceTasks = [{ id: 2, dependencies: [], title: 'Task 2' }];
160 | const conflicts = findCrossTagDependencies(
161 | sourceTasks,
162 | 'backlog',
163 | 'done',
164 | mockAllTasks
165 | );
166 |
167 | expect(conflicts).toHaveLength(0);
168 | });
169 |
170 | it('should throw error for invalid sourceTasks parameter', () => {
171 | expect(() =>
172 | findCrossTagDependencies(
173 | 'not-an-array',
174 | 'backlog',
175 | 'done',
176 | mockAllTasks
177 | )
178 | ).toThrow('Source tasks parameter must be an array');
179 | });
180 |
181 | it('should throw error for invalid source tag', () => {
182 | const sourceTasks = [{ id: 1, dependencies: [], title: 'Task 1' }];
183 | expect(() =>
184 | findCrossTagDependencies(sourceTasks, '', 'done', mockAllTasks)
185 | ).toThrow('Source tag must be a valid string');
186 | });
187 |
188 | it('should throw error for invalid target tag', () => {
189 | const sourceTasks = [{ id: 1, dependencies: [], title: 'Task 1' }];
190 | expect(() =>
191 | findCrossTagDependencies(sourceTasks, 'backlog', null, mockAllTasks)
192 | ).toThrow('Target tag must be a valid string');
193 | });
194 |
195 | it('should throw error for invalid allTasks parameter', () => {
196 | const sourceTasks = [{ id: 1, dependencies: [], title: 'Task 1' }];
197 | expect(() =>
198 | findCrossTagDependencies(sourceTasks, 'backlog', 'done', 'not-an-array')
199 | ).toThrow('All tasks parameter must be an array');
200 | });
201 | });
202 |
203 | describe('getDependentTaskIds', () => {
204 | const mockAllTasks = [
205 | { id: 1, tag: 'backlog', dependencies: [2], title: 'Task 1' },
206 | { id: 2, tag: 'backlog', dependencies: [], title: 'Task 2' },
207 | { id: 3, tag: 'in-progress', dependencies: [1], title: 'Task 3' },
208 | { id: 4, tag: 'done', dependencies: [], title: 'Task 4' }
209 | ];
210 |
211 | it('should return dependent task IDs', () => {
212 | const sourceTasks = [{ id: 1, dependencies: [2], title: 'Task 1' }];
213 | const crossTagDependencies = [
214 | { taskId: 1, dependencyId: 2, dependencyTag: 'backlog' }
215 | ];
216 | const dependentIds = getDependentTaskIds(
217 | sourceTasks,
218 | crossTagDependencies,
219 | mockAllTasks
220 | );
221 |
222 | expect(dependentIds).toContain(2);
223 | // The function also finds tasks that depend on the source task, so we expect more than just the dependency
224 | expect(dependentIds.length).toBeGreaterThan(0);
225 | });
226 |
227 | it('should handle multiple dependencies with recursive resolution', () => {
228 | const sourceTasks = [{ id: 5, dependencies: [1, 3], title: 'Task 5' }];
229 | const crossTagDependencies = [
230 | { taskId: 5, dependencyId: 1, dependencyTag: 'backlog' },
231 | { taskId: 5, dependencyId: 3, dependencyTag: 'in-progress' }
232 | ];
233 | const dependentIds = getDependentTaskIds(
234 | sourceTasks,
235 | crossTagDependencies,
236 | mockAllTasks
237 | );
238 |
239 | // Should find all dependencies recursively:
240 | // Task 5 → [1, 3], Task 1 → [2], so total is [1, 2, 3]
241 | expect(dependentIds).toContain(1);
242 | expect(dependentIds).toContain(2); // Task 1's dependency
243 | expect(dependentIds).toContain(3);
244 | expect(dependentIds).toHaveLength(3);
245 | });
246 |
247 | it('should return empty array when no dependencies', () => {
248 | const sourceTasks = [{ id: 2, dependencies: [], title: 'Task 2' }];
249 | const crossTagDependencies = [];
250 | const dependentIds = getDependentTaskIds(
251 | sourceTasks,
252 | crossTagDependencies,
253 | mockAllTasks
254 | );
255 |
256 | // The function finds tasks that depend on source tasks, so even with no cross-tag dependencies,
257 | // it might find tasks that depend on the source task
258 | expect(Array.isArray(dependentIds)).toBe(true);
259 | });
260 |
261 | it('should throw error for invalid sourceTasks parameter', () => {
262 | const crossTagDependencies = [];
263 | expect(() =>
264 | getDependentTaskIds('not-an-array', crossTagDependencies, mockAllTasks)
265 | ).toThrow('Source tasks parameter must be an array');
266 | });
267 |
268 | it('should throw error for invalid crossTagDependencies parameter', () => {
269 | const sourceTasks = [{ id: 1, dependencies: [], title: 'Task 1' }];
270 | expect(() =>
271 | getDependentTaskIds(sourceTasks, 'not-an-array', mockAllTasks)
272 | ).toThrow('Cross tag dependencies parameter must be an array');
273 | });
274 |
275 | it('should throw error for invalid allTasks parameter', () => {
276 | const sourceTasks = [{ id: 1, dependencies: [], title: 'Task 1' }];
277 | const crossTagDependencies = [];
278 | expect(() =>
279 | getDependentTaskIds(sourceTasks, crossTagDependencies, 'not-an-array')
280 | ).toThrow('All tasks parameter must be an array');
281 | });
282 | });
283 |
284 | describe('validateSubtaskMove', () => {
285 | it('should throw error for subtask movement', () => {
286 | expect(() =>
287 | validateSubtaskMove('1.2', 'backlog', 'in-progress')
288 | ).toThrow('Cannot move subtask 1.2 directly between tags');
289 | });
290 |
291 | it('should allow regular task movement', () => {
292 | expect(() =>
293 | validateSubtaskMove('1', 'backlog', 'in-progress')
294 | ).not.toThrow();
295 | });
296 |
297 | it('should throw error for invalid taskId parameter', () => {
298 | expect(() => validateSubtaskMove(null, 'backlog', 'in-progress')).toThrow(
299 | 'Task ID must be a valid string'
300 | );
301 | });
302 |
303 | it('should throw error for invalid source tag', () => {
304 | expect(() => validateSubtaskMove('1', '', 'in-progress')).toThrow(
305 | 'Source tag must be a valid string'
306 | );
307 | });
308 |
309 | it('should throw error for invalid target tag', () => {
310 | expect(() => validateSubtaskMove('1', 'backlog', null)).toThrow(
311 | 'Target tag must be a valid string'
312 | );
313 | });
314 | });
315 |
316 | describe('canMoveWithDependencies', () => {
317 | const mockAllTasks = [
318 | { id: 1, tag: 'backlog', dependencies: [2], title: 'Task 1' },
319 | { id: 2, tag: 'backlog', dependencies: [], title: 'Task 2' },
320 | { id: 3, tag: 'in-progress', dependencies: [1], title: 'Task 3' },
321 | { id: 4, tag: 'done', dependencies: [], title: 'Task 4' }
322 | ];
323 |
324 | it('should return canMove: true when no conflicts exist', () => {
325 | const result = canMoveWithDependencies(
326 | '2',
327 | 'backlog',
328 | 'in-progress',
329 | mockAllTasks
330 | );
331 |
332 | expect(result.canMove).toBe(true);
333 | expect(result.dependentTaskIds).toHaveLength(0);
334 | expect(result.conflicts).toHaveLength(0);
335 | });
336 |
337 | it('should return canMove: false when conflicts exist', () => {
338 | const result = canMoveWithDependencies(
339 | '1',
340 | 'backlog',
341 | 'in-progress',
342 | mockAllTasks
343 | );
344 |
345 | expect(result.canMove).toBe(false);
346 | expect(result.dependentTaskIds).toContain(2);
347 | expect(result.conflicts).toHaveLength(1);
348 | });
349 |
350 | it('should return canMove: false when task not found', () => {
351 | const result = canMoveWithDependencies(
352 | '999',
353 | 'backlog',
354 | 'in-progress',
355 | mockAllTasks
356 | );
357 |
358 | expect(result.canMove).toBe(false);
359 | expect(result.error).toBe('Task not found');
360 | });
361 |
362 | it('should handle string task IDs', () => {
363 | const result = canMoveWithDependencies(
364 | '2',
365 | 'backlog',
366 | 'in-progress',
367 | mockAllTasks
368 | );
369 |
370 | expect(result.canMove).toBe(true);
371 | });
372 |
373 | it('should throw error for invalid taskId parameter', () => {
374 | expect(() =>
375 | canMoveWithDependencies(null, 'backlog', 'in-progress', mockAllTasks)
376 | ).toThrow('Task ID must be a valid string');
377 | });
378 |
379 | it('should throw error for invalid source tag', () => {
380 | expect(() =>
381 | canMoveWithDependencies('1', '', 'in-progress', mockAllTasks)
382 | ).toThrow('Source tag must be a valid string');
383 | });
384 |
385 | it('should throw error for invalid target tag', () => {
386 | expect(() =>
387 | canMoveWithDependencies('1', 'backlog', null, mockAllTasks)
388 | ).toThrow('Target tag must be a valid string');
389 | });
390 |
391 | it('should throw error for invalid allTasks parameter', () => {
392 | expect(() =>
393 | canMoveWithDependencies('1', 'backlog', 'in-progress', 'not-an-array')
394 | ).toThrow('All tasks parameter must be an array');
395 | });
396 | });
397 | });
398 |
```
--------------------------------------------------------------------------------
/src/utils/stream-parser.js:
--------------------------------------------------------------------------------
```javascript
1 | import { JSONParser } from '@streamparser/json';
2 |
3 | /**
4 | * Custom error class for streaming-related failures
5 | * Provides error codes for robust error handling without string matching
6 | */
7 | export class StreamingError extends Error {
8 | constructor(message, code) {
9 | super(message);
10 | this.name = 'StreamingError';
11 | this.code = code;
12 |
13 | // Maintain proper stack trace (V8 engines)
14 | if (Error.captureStackTrace) {
15 | Error.captureStackTrace(this, StreamingError);
16 | }
17 | }
18 | }
19 |
20 | /**
21 | * Standard streaming error codes
22 | */
23 | export const STREAMING_ERROR_CODES = {
24 | NOT_ASYNC_ITERABLE: 'STREAMING_NOT_SUPPORTED',
25 | STREAM_PROCESSING_FAILED: 'STREAM_PROCESSING_FAILED',
26 | STREAM_NOT_ITERABLE: 'STREAM_NOT_ITERABLE',
27 | BUFFER_SIZE_EXCEEDED: 'BUFFER_SIZE_EXCEEDED'
28 | };
29 |
30 | /**
31 | * Default maximum buffer size (1MB)
32 | */
33 | export const DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024; // 1MB in bytes
34 |
35 | /**
36 | * Configuration options for the streaming JSON parser
37 | */
38 | class StreamParserConfig {
39 | constructor(config = {}) {
40 | this.jsonPaths = config.jsonPaths;
41 | this.onProgress = config.onProgress;
42 | this.onError = config.onError;
43 | this.estimateTokens =
44 | config.estimateTokens || ((text) => Math.ceil(text.length / 4));
45 | this.expectedTotal = config.expectedTotal || 0;
46 | this.fallbackItemExtractor = config.fallbackItemExtractor;
47 | this.itemValidator =
48 | config.itemValidator || StreamParserConfig.defaultItemValidator;
49 | this.maxBufferSize = config.maxBufferSize || DEFAULT_MAX_BUFFER_SIZE;
50 |
51 | this.validate();
52 | }
53 |
54 | validate() {
55 | if (!this.jsonPaths || !Array.isArray(this.jsonPaths)) {
56 | throw new Error('jsonPaths is required and must be an array');
57 | }
58 | if (this.jsonPaths.length === 0) {
59 | throw new Error('jsonPaths array cannot be empty');
60 | }
61 | if (this.maxBufferSize <= 0) {
62 | throw new Error('maxBufferSize must be positive');
63 | }
64 | if (this.expectedTotal < 0) {
65 | throw new Error('expectedTotal cannot be negative');
66 | }
67 | if (this.estimateTokens && typeof this.estimateTokens !== 'function') {
68 | throw new Error('estimateTokens must be a function');
69 | }
70 | if (this.onProgress && typeof this.onProgress !== 'function') {
71 | throw new Error('onProgress must be a function');
72 | }
73 | if (this.onError && typeof this.onError !== 'function') {
74 | throw new Error('onError must be a function');
75 | }
76 | if (
77 | this.fallbackItemExtractor &&
78 | typeof this.fallbackItemExtractor !== 'function'
79 | ) {
80 | throw new Error('fallbackItemExtractor must be a function');
81 | }
82 | if (this.itemValidator && typeof this.itemValidator !== 'function') {
83 | throw new Error('itemValidator must be a function');
84 | }
85 | }
86 |
87 | static defaultItemValidator(item) {
88 | return (
89 | item && item.title && typeof item.title === 'string' && item.title.trim()
90 | );
91 | }
92 | }
93 |
94 | /**
95 | * Manages progress tracking and metadata
96 | */
97 | class ProgressTracker {
98 | constructor(config) {
99 | this.onProgress = config.onProgress;
100 | this.onError = config.onError;
101 | this.estimateTokens = config.estimateTokens;
102 | this.expectedTotal = config.expectedTotal;
103 | this.parsedItems = [];
104 | this.accumulatedText = '';
105 | }
106 |
107 | addItem(item) {
108 | this.parsedItems.push(item);
109 | this.reportProgress(item);
110 | }
111 |
112 | addText(chunk) {
113 | this.accumulatedText += chunk;
114 | }
115 |
116 | getMetadata() {
117 | return {
118 | currentCount: this.parsedItems.length,
119 | expectedTotal: this.expectedTotal,
120 | accumulatedText: this.accumulatedText,
121 | estimatedTokens: this.estimateTokens(this.accumulatedText)
122 | };
123 | }
124 |
125 | reportProgress(item) {
126 | if (!this.onProgress) return;
127 |
128 | try {
129 | this.onProgress(item, this.getMetadata());
130 | } catch (progressError) {
131 | this.handleProgressError(progressError);
132 | }
133 | }
134 |
135 | handleProgressError(error) {
136 | if (this.onError) {
137 | this.onError(new Error(`Progress callback failed: ${error.message}`));
138 | }
139 | }
140 | }
141 |
142 | /**
143 | * Handles stream processing with different stream types
144 | */
145 | class StreamProcessor {
146 | constructor(onChunk) {
147 | this.onChunk = onChunk;
148 | }
149 |
150 | async process(textStream) {
151 | const streamHandler = this.detectStreamType(textStream);
152 | await streamHandler(textStream);
153 | }
154 |
155 | detectStreamType(textStream) {
156 | // Check for textStream property
157 | if (this.hasAsyncIterator(textStream?.textStream)) {
158 | return (stream) => this.processTextStream(stream.textStream);
159 | }
160 |
161 | // Check for fullStream property
162 | if (this.hasAsyncIterator(textStream?.fullStream)) {
163 | return (stream) => this.processFullStream(stream.fullStream);
164 | }
165 |
166 | // Check if stream itself is iterable
167 | if (this.hasAsyncIterator(textStream)) {
168 | return (stream) => this.processDirectStream(stream);
169 | }
170 |
171 | throw new StreamingError(
172 | 'Stream object is not iterable - no textStream, fullStream, or direct async iterator found',
173 | STREAMING_ERROR_CODES.STREAM_NOT_ITERABLE
174 | );
175 | }
176 |
177 | hasAsyncIterator(obj) {
178 | return obj && typeof obj[Symbol.asyncIterator] === 'function';
179 | }
180 |
181 | async processTextStream(stream) {
182 | for await (const chunk of stream) {
183 | this.onChunk(chunk);
184 | }
185 | }
186 |
187 | async processFullStream(stream) {
188 | for await (const chunk of stream) {
189 | if (chunk.type === 'text-delta' && chunk.textDelta) {
190 | this.onChunk(chunk.textDelta);
191 | }
192 | }
193 | }
194 |
195 | async processDirectStream(stream) {
196 | for await (const chunk of stream) {
197 | this.onChunk(chunk);
198 | }
199 | }
200 | }
201 |
202 | /**
203 | * Manages JSON parsing with the streaming parser
204 | */
205 | class JSONStreamParser {
206 | constructor(config, progressTracker) {
207 | this.config = config;
208 | this.progressTracker = progressTracker;
209 | this.parser = new JSONParser({ paths: config.jsonPaths });
210 | this.setupHandlers();
211 | }
212 |
213 | setupHandlers() {
214 | this.parser.onValue = (value, key, parent, stack) => {
215 | this.handleParsedValue(value);
216 | };
217 |
218 | this.parser.onError = (error) => {
219 | this.handleParseError(error);
220 | };
221 | }
222 |
223 | handleParsedValue(value) {
224 | // Extract the actual item object from the parser's nested structure
225 | const item = value.value || value;
226 |
227 | if (this.config.itemValidator(item)) {
228 | this.progressTracker.addItem(item);
229 | }
230 | }
231 |
232 | handleParseError(error) {
233 | if (this.config.onError) {
234 | this.config.onError(new Error(`JSON parsing error: ${error.message}`));
235 | }
236 | // Don't throw here - we'll handle this in the fallback logic
237 | }
238 |
239 | write(chunk) {
240 | this.parser.write(chunk);
241 | }
242 |
243 | end() {
244 | this.parser.end();
245 | }
246 | }
247 |
248 | /**
249 | * Handles fallback parsing when streaming fails
250 | */
251 | class FallbackParser {
252 | constructor(config, progressTracker) {
253 | this.config = config;
254 | this.progressTracker = progressTracker;
255 | }
256 |
257 | async attemptParsing() {
258 | if (!this.shouldAttemptFallback()) {
259 | return [];
260 | }
261 |
262 | try {
263 | return await this.parseFallbackItems();
264 | } catch (parseError) {
265 | this.handleFallbackError(parseError);
266 | return [];
267 | }
268 | }
269 |
270 | shouldAttemptFallback() {
271 | return (
272 | this.config.expectedTotal > 0 &&
273 | this.progressTracker.parsedItems.length < this.config.expectedTotal &&
274 | this.progressTracker.accumulatedText &&
275 | this.config.fallbackItemExtractor
276 | );
277 | }
278 |
279 | async parseFallbackItems() {
280 | const jsonText = this._cleanJsonText(this.progressTracker.accumulatedText);
281 | const fullResponse = JSON.parse(jsonText);
282 | const fallbackItems = this.config.fallbackItemExtractor(fullResponse);
283 |
284 | if (!Array.isArray(fallbackItems)) {
285 | return [];
286 | }
287 |
288 | return this._processNewItems(fallbackItems);
289 | }
290 |
291 | _cleanJsonText(text) {
292 | // Remove markdown code block wrappers and trim whitespace
293 | return text
294 | .replace(/^```(?:json)?\s*\n?/i, '')
295 | .replace(/\n?```\s*$/i, '')
296 | .trim();
297 | }
298 |
299 | _processNewItems(fallbackItems) {
300 | // Only add items we haven't already parsed
301 | const itemsToAdd = fallbackItems.slice(
302 | this.progressTracker.parsedItems.length
303 | );
304 | const newItems = [];
305 |
306 | for (const item of itemsToAdd) {
307 | if (this.config.itemValidator(item)) {
308 | newItems.push(item);
309 | this.progressTracker.addItem(item);
310 | }
311 | }
312 |
313 | return newItems;
314 | }
315 |
316 | handleFallbackError(error) {
317 | if (this.progressTracker.parsedItems.length === 0) {
318 | throw new Error(`Failed to parse AI response as JSON: ${error.message}`);
319 | }
320 | // If we have some items from streaming, continue with those
321 | }
322 | }
323 |
324 | /**
325 | * Buffer size validator
326 | */
327 | class BufferSizeValidator {
328 | constructor(maxSize) {
329 | this.maxSize = maxSize;
330 | this.currentSize = 0;
331 | }
332 |
333 | validateChunk(existingText, newChunk) {
334 | const newSize = Buffer.byteLength(existingText + newChunk, 'utf8');
335 |
336 | if (newSize > this.maxSize) {
337 | throw new StreamingError(
338 | `Buffer size exceeded: ${newSize} bytes > ${this.maxSize} bytes maximum`,
339 | STREAMING_ERROR_CODES.BUFFER_SIZE_EXCEEDED
340 | );
341 | }
342 |
343 | this.currentSize = newSize;
344 | }
345 | }
346 |
347 | /**
348 | * Main orchestrator for stream parsing
349 | */
350 | class StreamParserOrchestrator {
351 | constructor(config) {
352 | this.config = new StreamParserConfig(config);
353 | this.progressTracker = new ProgressTracker(this.config);
354 | this.bufferValidator = new BufferSizeValidator(this.config.maxBufferSize);
355 | this.jsonParser = new JSONStreamParser(this.config, this.progressTracker);
356 | this.fallbackParser = new FallbackParser(this.config, this.progressTracker);
357 | }
358 |
359 | async parse(textStream) {
360 | if (!textStream) {
361 | throw new Error('No text stream provided');
362 | }
363 |
364 | await this.processStream(textStream);
365 | await this.waitForParsingCompletion();
366 |
367 | const usedFallback = await this.attemptFallbackIfNeeded();
368 |
369 | return this.buildResult(usedFallback);
370 | }
371 |
372 | async processStream(textStream) {
373 | const processor = new StreamProcessor((chunk) => {
374 | this.bufferValidator.validateChunk(
375 | this.progressTracker.accumulatedText,
376 | chunk
377 | );
378 | this.progressTracker.addText(chunk);
379 | this.jsonParser.write(chunk);
380 | });
381 |
382 | try {
383 | await processor.process(textStream);
384 | } catch (streamError) {
385 | this.handleStreamError(streamError);
386 | }
387 |
388 | this.jsonParser.end();
389 | }
390 |
391 | handleStreamError(error) {
392 | // Re-throw StreamingError as-is, wrap other errors
393 | if (error instanceof StreamingError) {
394 | throw error;
395 | }
396 | throw new StreamingError(
397 | `Failed to process AI text stream: ${error.message}`,
398 | STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED
399 | );
400 | }
401 |
402 | async waitForParsingCompletion() {
403 | // Wait for final parsing to complete (JSON parser may still be processing)
404 | await new Promise((resolve) => setTimeout(resolve, 100));
405 | }
406 |
407 | async attemptFallbackIfNeeded() {
408 | const fallbackItems = await this.fallbackParser.attemptParsing();
409 | return fallbackItems.length > 0;
410 | }
411 |
412 | buildResult(usedFallback) {
413 | const metadata = this.progressTracker.getMetadata();
414 |
415 | return {
416 | items: this.progressTracker.parsedItems,
417 | accumulatedText: metadata.accumulatedText,
418 | estimatedTokens: metadata.estimatedTokens,
419 | usedFallback
420 | };
421 | }
422 | }
423 |
424 | /**
425 | * Parse a streaming JSON response with progress tracking
426 | *
427 | * Example with custom buffer size:
428 | * ```js
429 | * const result = await parseStream(stream, {
430 | * jsonPaths: ['$.tasks.*'],
431 | * maxBufferSize: 2 * 1024 * 1024 // 2MB
432 | * });
433 | * ```
434 | *
435 | * @param {Object} textStream - The AI service text stream object
436 | * @param {Object} config - Configuration options
437 | * @returns {Promise<Object>} Parsed result with metadata
438 | */
439 | export async function parseStream(textStream, config = {}) {
440 | const orchestrator = new StreamParserOrchestrator(config);
441 | return orchestrator.parse(textStream);
442 | }
443 |
444 | /**
445 | * Process different types of text streams
446 | * @param {Object} textStream - The stream object from AI service
447 | * @param {Function} onChunk - Callback for each text chunk
448 | */
449 | export async function processTextStream(textStream, onChunk) {
450 | const processor = new StreamProcessor(onChunk);
451 | await processor.process(textStream);
452 | }
453 |
454 | /**
455 | * Attempt fallback JSON parsing when streaming parsing is incomplete
456 | * @param {string} accumulatedText - Complete accumulated text
457 | * @param {Array} existingItems - Items already parsed from streaming
458 | * @param {number} expectedTotal - Expected total number of items
459 | * @param {Object} config - Configuration for progress reporting
460 | * @returns {Promise<Array>} Additional items found via fallback parsing
461 | */
462 | export async function attemptFallbackParsing(
463 | accumulatedText,
464 | existingItems,
465 | expectedTotal,
466 | config
467 | ) {
468 | // Create a temporary progress tracker for backward compatibility
469 | const progressTracker = new ProgressTracker({
470 | onProgress: config.onProgress,
471 | onError: config.onError,
472 | estimateTokens: config.estimateTokens,
473 | expectedTotal
474 | });
475 |
476 | progressTracker.parsedItems = existingItems;
477 | progressTracker.accumulatedText = accumulatedText;
478 |
479 | const fallbackParser = new FallbackParser(
480 | {
481 | ...config,
482 | expectedTotal,
483 | itemValidator:
484 | config.itemValidator || StreamParserConfig.defaultItemValidator
485 | },
486 | progressTracker
487 | );
488 |
489 | return fallbackParser.attemptParsing();
490 | }
491 |
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/auth/managers/auth-manager.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * Authentication manager for Task Master CLI
3 | */
4 |
5 | import fs from 'fs';
6 | import os from 'os';
7 | import path from 'path';
8 | import {
9 | ERROR_CODES,
10 | TaskMasterError
11 | } from '../../../common/errors/task-master-error.js';
12 | import { getLogger } from '../../../common/logger/index.js';
13 | import type { Brief } from '../../briefs/types.js';
14 | import { SupabaseAuthClient } from '../../integration/clients/supabase-client.js';
15 | import { ContextStore } from '../services/context-store.js';
16 | import { OAuthService } from '../services/oauth-service.js';
17 | import {
18 | type Organization,
19 | OrganizationService,
20 | type RemoteTask
21 | } from '../services/organization.service.js';
22 | import {
23 | AuthConfig,
24 | AuthCredentials,
25 | AuthenticationError,
26 | OAuthFlowOptions,
27 | UserContext,
28 | UserContextWithBrief
29 | } from '../types.js';
30 |
31 | /**
32 | * Authentication manager class
33 | */
34 | export class AuthManager {
35 | private static instance: AuthManager | null = null;
36 | private static readonly staticLogger = getLogger('AuthManager');
37 | private contextStore: ContextStore;
38 | private oauthService: OAuthService;
39 | public supabaseClient: SupabaseAuthClient;
40 | private organizationService?: OrganizationService;
41 | private readonly logger = getLogger('AuthManager');
42 | private readonly LEGACY_AUTH_FILE = path.join(
43 | os.homedir(),
44 | '.taskmaster',
45 | 'auth.json'
46 | );
47 |
48 | private constructor(config?: Partial<AuthConfig>) {
49 | this.contextStore = ContextStore.getInstance();
50 | this.supabaseClient = new SupabaseAuthClient();
51 | // Pass the supabase client to OAuthService so they share the same instance
52 | this.oauthService = new OAuthService(
53 | this.contextStore,
54 | this.supabaseClient,
55 | config
56 | );
57 |
58 | // Initialize Supabase client with session restoration
59 | // Fire-and-forget with catch handler to prevent unhandled rejections
60 | this.initializeSupabaseSession().catch(() => {
61 | // Errors are already logged in initializeSupabaseSession
62 | });
63 |
64 | // Migrate legacy auth.json if it exists
65 | // Fire-and-forget with catch handler
66 | this.migrateLegacyAuth().catch(() => {
67 | // Errors are already logged in migrateLegacyAuth
68 | });
69 | }
70 |
71 | /**
72 | * Initialize Supabase session from stored credentials
73 | */
74 | private async initializeSupabaseSession(): Promise<void> {
75 | try {
76 | await this.supabaseClient.initialize();
77 | } catch (error) {
78 | // Log but don't throw - session might not exist yet
79 | this.logger.debug('No existing session to restore');
80 | }
81 | }
82 |
83 | /**
84 | * Migrate legacy auth.json to Supabase session
85 | * Called once during AuthManager initialization
86 | */
87 | private async migrateLegacyAuth(): Promise<void> {
88 | if (!fs.existsSync(this.LEGACY_AUTH_FILE)) {
89 | return;
90 | }
91 |
92 | try {
93 | // If we have a valid Supabase session, delete legacy file
94 | const hasSession = await this.hasValidSession();
95 | if (hasSession) {
96 | fs.unlinkSync(this.LEGACY_AUTH_FILE);
97 | this.logger.info('Migrated to Supabase auth, removed legacy auth.json');
98 | return;
99 | }
100 |
101 | // Otherwise, user needs to re-authenticate
102 | this.logger.warn('Legacy auth.json found but no valid Supabase session.');
103 | this.logger.warn('Please run: task-master auth login');
104 | } catch (error) {
105 | this.logger.debug('Error during legacy auth migration:', error);
106 | }
107 | }
108 |
109 | /**
110 | * Get singleton instance
111 | */
112 | static getInstance(config?: Partial<AuthConfig>): AuthManager {
113 | if (!AuthManager.instance) {
114 | AuthManager.instance = new AuthManager(config);
115 | } else if (config) {
116 | // Warn if config is provided after initialization
117 | AuthManager.staticLogger.warn(
118 | 'getInstance called with config after initialization; config is ignored.'
119 | );
120 | }
121 | return AuthManager.instance;
122 | }
123 |
124 | /**
125 | * Reset the singleton instance (useful for testing)
126 | */
127 | static resetInstance(): void {
128 | AuthManager.instance = null;
129 | ContextStore.resetInstance();
130 | }
131 |
132 | /**
133 | * Get access token from current Supabase session
134 | * @returns Access token or null if not authenticated
135 | */
136 | async getAccessToken(): Promise<string | null> {
137 | const session = await this.supabaseClient.getSession();
138 | return session?.access_token || null;
139 | }
140 |
141 | /**
142 | * Get authentication credentials from Supabase session
143 | * Modern replacement for legacy getCredentials()
144 | * @returns AuthCredentials object or null if not authenticated
145 | */
146 | async getAuthCredentials(): Promise<AuthCredentials | null> {
147 | const session = await this.supabaseClient.getSession();
148 | if (!session) return null;
149 |
150 | const user = session.user;
151 | const context = this.contextStore.getUserContext();
152 |
153 | return {
154 | token: session.access_token,
155 | refreshToken: session.refresh_token,
156 | userId: user.id,
157 | email: user.email,
158 | expiresAt: session.expires_at
159 | ? new Date(session.expires_at * 1000).toISOString()
160 | : undefined,
161 | tokenType: 'standard',
162 | savedAt: new Date().toISOString(),
163 | selectedContext: context || undefined
164 | };
165 | }
166 |
167 | /**
168 | * Start OAuth 2.0 Authorization Code Flow with browser handling
169 | */
170 | async authenticateWithOAuth(
171 | options: OAuthFlowOptions = {}
172 | ): Promise<AuthCredentials> {
173 | return this.oauthService.authenticate(options);
174 | }
175 |
176 | /**
177 | * Authenticate using a one-time token
178 | * This is useful for CLI authentication in SSH/remote environments
179 | * where browser-based auth is not practical
180 | */
181 | async authenticateWithCode(token: string): Promise<AuthCredentials> {
182 | try {
183 | this.logger.info('Authenticating with one-time token...');
184 |
185 | // Verify the token and get session from Supabase
186 | const session = await this.supabaseClient.verifyOneTimeCode(token);
187 |
188 | if (!session || !session.access_token) {
189 | throw new AuthenticationError(
190 | 'Failed to obtain access token from token',
191 | 'NO_TOKEN'
192 | );
193 | }
194 |
195 | // Get user information
196 | const user = await this.supabaseClient.getUser();
197 |
198 | if (!user) {
199 | throw new AuthenticationError(
200 | 'Failed to get user information',
201 | 'INVALID_RESPONSE'
202 | );
203 | }
204 |
205 | // Store user context
206 | this.contextStore.saveContext({
207 | userId: user.id,
208 | email: user.email
209 | });
210 |
211 | // Build credentials response
212 | const context = this.contextStore.getUserContext();
213 | const credentials: AuthCredentials = {
214 | token: session.access_token,
215 | refreshToken: session.refresh_token,
216 | userId: user.id,
217 | email: user.email,
218 | expiresAt: session.expires_at
219 | ? new Date(session.expires_at * 1000).toISOString()
220 | : undefined,
221 | tokenType: 'standard',
222 | savedAt: new Date().toISOString(),
223 | selectedContext: context || undefined
224 | };
225 |
226 | this.logger.info('Successfully authenticated with token');
227 | return credentials;
228 | } catch (error) {
229 | if (error instanceof AuthenticationError) {
230 | throw error;
231 | }
232 | throw new AuthenticationError(
233 | `Token authentication failed: ${(error as Error).message}`,
234 | 'CODE_AUTH_FAILED'
235 | );
236 | }
237 | }
238 |
239 | /**
240 | * Get the authorization URL (for browser opening)
241 | */
242 | getAuthorizationUrl(): string | null {
243 | return this.oauthService.getAuthorizationUrl();
244 | }
245 |
246 | /**
247 | * Refresh authentication token using Supabase session
248 | * Note: Supabase handles token refresh automatically via the session storage adapter.
249 | * This method is mainly for explicit refresh requests.
250 | */
251 | async refreshToken(): Promise<AuthCredentials> {
252 | try {
253 | // Use Supabase's built-in session refresh
254 | const session = await this.supabaseClient.refreshSession();
255 |
256 | if (!session) {
257 | throw new AuthenticationError(
258 | 'Failed to refresh session',
259 | 'REFRESH_FAILED'
260 | );
261 | }
262 |
263 | // Sync user info to context store
264 | this.contextStore.saveContext({
265 | userId: session.user.id,
266 | email: session.user.email
267 | });
268 |
269 | // Build credentials response
270 | const context = this.contextStore.getContext();
271 | const credentials: AuthCredentials = {
272 | token: session.access_token,
273 | refreshToken: session.refresh_token,
274 | userId: session.user.id,
275 | email: session.user.email,
276 | expiresAt: session.expires_at
277 | ? new Date(session.expires_at * 1000).toISOString()
278 | : undefined,
279 | savedAt: new Date().toISOString(),
280 | selectedContext: context?.selectedContext
281 | };
282 |
283 | return credentials;
284 | } catch (error) {
285 | if (error instanceof AuthenticationError) {
286 | throw error;
287 | }
288 | throw new AuthenticationError(
289 | `Token refresh failed: ${(error as Error).message}`,
290 | 'REFRESH_FAILED'
291 | );
292 | }
293 | }
294 |
295 | /**
296 | * Logout and clear credentials
297 | */
298 | async logout(): Promise<void> {
299 | try {
300 | // First try to sign out from Supabase to revoke tokens
301 | await this.supabaseClient.signOut();
302 | } catch (error) {
303 | // Log but don't throw - we still want to clear local credentials
304 | this.logger.warn('Failed to sign out from Supabase:', error);
305 | }
306 |
307 | // Clear app context
308 | this.contextStore.clearContext();
309 | // Session is cleared by supabaseClient.signOut()
310 |
311 | // Clear legacy auth.json if it exists
312 | try {
313 | if (fs.existsSync(this.LEGACY_AUTH_FILE)) {
314 | fs.unlinkSync(this.LEGACY_AUTH_FILE);
315 | this.logger.debug('Cleared legacy auth.json');
316 | }
317 | } catch (error) {
318 | // Ignore errors clearing legacy file
319 | this.logger.debug('No legacy credentials to clear');
320 | }
321 | }
322 |
323 | /**
324 | * Check if valid Supabase session exists
325 | * @returns true if a valid session exists
326 | */
327 | async hasValidSession(): Promise<boolean> {
328 | try {
329 | const session = await this.supabaseClient.getSession();
330 | return session !== null;
331 | } catch {
332 | return false;
333 | }
334 | }
335 |
336 | /**
337 | * Get the current Supabase session
338 | */
339 | async getSession() {
340 | return this.supabaseClient.getSession();
341 | }
342 |
343 | /**
344 | * Get stored user context (userId, email)
345 | */
346 | getStoredContext() {
347 | return this.contextStore.getContext();
348 | }
349 |
350 | /**
351 | * Get the current user context (org/brief selection)
352 | */
353 | getContext(): UserContext | null {
354 | return this.contextStore.getUserContext();
355 | }
356 |
357 | /**
358 | * Update the user context (org/brief selection)
359 | */
360 | async updateContext(context: Partial<UserContext>): Promise<void> {
361 | if (!(await this.hasValidSession())) {
362 | throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
363 | }
364 |
365 | this.contextStore.updateUserContext(context);
366 | }
367 |
368 | /**
369 | * Clear the user context
370 | */
371 | async clearContext(): Promise<void> {
372 | if (!(await this.hasValidSession())) {
373 | throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
374 | }
375 |
376 | this.contextStore.clearUserContext();
377 | }
378 |
379 | /**
380 | * Get the organization service instance
381 | * Uses the Supabase client with the current session
382 | */
383 | private async getOrganizationService(): Promise<OrganizationService> {
384 | if (!this.organizationService) {
385 | // Check if we have a valid Supabase session
386 | const session = await this.supabaseClient.getSession();
387 |
388 | if (!session) {
389 | throw new AuthenticationError('Not authenticated', 'NOT_AUTHENTICATED');
390 | }
391 |
392 | // Use the SupabaseAuthClient which now has the session
393 | const supabaseClient = this.supabaseClient.getClient();
394 | this.organizationService = new OrganizationService(supabaseClient as any);
395 | }
396 | return this.organizationService;
397 | }
398 |
399 | /**
400 | * Get all organizations for the authenticated user
401 | */
402 | async getOrganizations(): Promise<Organization[]> {
403 | const service = await this.getOrganizationService();
404 | return service.getOrganizations();
405 | }
406 |
407 | /**
408 | * Get all briefs for a specific organization
409 | */
410 | async getBriefs(orgId: string): Promise<Brief[]> {
411 | const service = await this.getOrganizationService();
412 | return service.getBriefs(orgId);
413 | }
414 |
415 | /**
416 | * Get a specific organization by ID
417 | */
418 | async getOrganization(orgId: string): Promise<Organization | null> {
419 | const service = await this.getOrganizationService();
420 | return service.getOrganization(orgId);
421 | }
422 |
423 | /**
424 | * Get a specific brief by ID
425 | */
426 | async getBrief(briefId: string): Promise<Brief | null> {
427 | const service = await this.getOrganizationService();
428 | return service.getBrief(briefId);
429 | }
430 |
431 | /**
432 | * Get all tasks for a specific brief
433 | */
434 | async getTasks(briefId: string): Promise<RemoteTask[]> {
435 | const service = await this.getOrganizationService();
436 | return service.getTasks(briefId);
437 | }
438 |
439 | /**
440 | * Ensure a brief is selected in the current context
441 | * Throws a TaskMasterError if no brief is selected
442 | * @param operation - The operation name for error context
443 | * @returns The current user context with a guaranteed briefId
444 | */
445 | ensureBriefSelected(operation: string): UserContextWithBrief {
446 | const context = this.getContext();
447 |
448 | if (!context?.briefId) {
449 | throw new TaskMasterError(
450 | 'No brief selected',
451 | ERROR_CODES.NO_BRIEF_SELECTED,
452 | {
453 | operation,
454 | userMessage:
455 | 'No brief selected. Please select a brief first using: tm context brief <brief-id> or tm context brief <brief-url>'
456 | }
457 | );
458 | }
459 |
460 | return context as UserContextWithBrief;
461 | }
462 | }
463 |
```
--------------------------------------------------------------------------------
/apps/cli/src/commands/tags.command.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Tags Command - Manage task organization with tags
3 | * Provides tag/brief management with file and API storage support
4 | */
5 |
6 | import { Command } from 'commander';
7 | import type { TmCore } from '@tm/core';
8 | import { createTmCore, getProjectPaths } from '@tm/core';
9 | import { displayError } from '../utils/index.js';
10 |
11 | /**
12 | * TODO: TECH DEBT - Architectural Refactor Needed
13 | *
14 | * Current State:
15 | * - This command imports legacy JS functions from scripts/modules/task-manager/tag-management.js
16 | * - These functions contain business logic that violates architecture guidelines (see CLAUDE.md)
17 | *
18 | * Target State:
19 | * - Move all business logic to TagService in @tm/core
20 | * - CLI should only handle presentation (argument parsing, output formatting)
21 | * - Remove dependency on legacy scripts/ directory
22 | *
23 | * Complexity:
24 | * - Legacy functions handle both API and file storage via bridge pattern
25 | * - Need to migrate API integration logic to @tm/core first
26 | * - Affects MCP layer as well (should share same @tm/core APIs)
27 | *
28 | * Priority: Medium (improves testability, maintainability, and code reuse)
29 | */
30 | import {
31 | createTag as legacyCreateTag,
32 | deleteTag as legacyDeleteTag,
33 | tags as legacyListTags,
34 | useTag as legacyUseTag,
35 | renameTag as legacyRenameTag,
36 | copyTag as legacyCopyTag
37 | } from '../../../../scripts/modules/task-manager/tag-management.js';
38 |
39 | /**
40 | * Result type from tags command
41 | */
42 | export interface TagsResult {
43 | success: boolean;
44 | action: 'list' | 'add' | 'use' | 'remove' | 'rename' | 'copy';
45 | tags?: any[];
46 | currentTag?: string | null;
47 | message?: string;
48 | }
49 |
50 | /**
51 | * Legacy function return types
52 | */
53 | interface LegacyListTagsResult {
54 | tags: any[];
55 | currentTag: string | null;
56 | totalTags: number;
57 | }
58 |
59 | interface LegacyUseTagResult {
60 | currentTag: string;
61 | }
62 |
63 | interface LegacyCreateTagOptions {
64 | description?: string;
65 | copyFromTag?: string;
66 | fromBranch?: boolean;
67 | }
68 |
69 | /**
70 | * TagsCommand - Manage tags/briefs for task organization
71 | */
72 | export class TagsCommand extends Command {
73 | private tmCore?: TmCore;
74 | private lastResult?: TagsResult;
75 | private throwOnError: boolean = false;
76 |
77 | constructor(name?: string) {
78 | super(name || 'tags');
79 |
80 | // Configure the command
81 | this.description('Manage tags for task organization');
82 |
83 | // Add subcommands
84 | this.addListCommand();
85 | this.addAddCommand();
86 | this.addUseCommand();
87 | this.addRemoveCommand();
88 | this.addRenameCommand();
89 | this.addCopyCommand();
90 |
91 | // Default action: list tags
92 | this.action(async () => {
93 | await this.executeList();
94 | });
95 | }
96 |
97 | /**
98 | * Add list subcommand
99 | */
100 | private addListCommand(): void {
101 | this.command('list')
102 | .description('List all tags with statistics (default action)')
103 | .option('--show-metadata', 'Show additional tag metadata')
104 | .addHelpText(
105 | 'after',
106 | `
107 | Examples:
108 | $ tm tags # List all tags (default)
109 | $ tm tags list # List all tags (explicit)
110 | $ tm tags list --show-metadata # List with metadata
111 | `
112 | )
113 | .action(async (options) => {
114 | await this.executeList(options);
115 | });
116 | }
117 |
118 | /**
119 | * Add add subcommand
120 | */
121 | private addAddCommand(): void {
122 | this.command('add')
123 | .description('Create a new tag')
124 | .argument('<name>', 'Name of the tag to create')
125 | .option('--description <desc>', 'Tag description')
126 | .option('--copy-from <tag>', 'Copy tasks from another tag')
127 | .option('--from-branch', 'Create tag from current git branch name')
128 | .addHelpText(
129 | 'after',
130 | `
131 | Examples:
132 | $ tm tags add feature-auth # Create new tag
133 | $ tm tags add sprint-2 --copy-from sprint-1 # Create with tasks copied
134 | $ tm tags add --from-branch # Create from current git branch
135 |
136 | Note: When using API storage, this will redirect you to the web UI to create a brief.
137 | `
138 | )
139 | .action(async (name, options) => {
140 | await this.executeAdd(name, options);
141 | });
142 | }
143 |
144 | /**
145 | * Add use subcommand
146 | */
147 | private addUseCommand(): void {
148 | this.command('use')
149 | .description('Switch to a different tag')
150 | .argument('<name>', 'Name or ID of the tag to switch to')
151 | .addHelpText(
152 | 'after',
153 | `
154 | Examples:
155 | $ tm tags use feature-auth # Switch by name
156 | $ tm tags use abc123 # Switch by ID (last 8 chars)
157 |
158 | Note: For API storage, this switches the active brief in your context.
159 | `
160 | )
161 | .action(async (name) => {
162 | await this.executeUse(name);
163 | });
164 | }
165 |
166 | /**
167 | * Add remove subcommand
168 | */
169 | private addRemoveCommand(): void {
170 | this.command('remove')
171 | .description('Remove a tag')
172 | .argument('<name>', 'Name or ID of the tag to remove')
173 | .option('-y, --yes', 'Skip confirmation prompt')
174 | .addHelpText(
175 | 'after',
176 | `
177 | Examples:
178 | $ tm tags remove old-feature # Remove tag with confirmation
179 | $ tm tags remove old-feature -y # Remove without confirmation
180 |
181 | Warning: This will delete all tasks in the tag!
182 | `
183 | )
184 | .action(async (name, options) => {
185 | await this.executeRemove(name, options);
186 | });
187 | }
188 |
189 | /**
190 | * Add rename subcommand
191 | */
192 | private addRenameCommand(): void {
193 | this.command('rename')
194 | .description('Rename a tag')
195 | .argument('<oldName>', 'Current tag name')
196 | .argument('<newName>', 'New tag name')
197 | .addHelpText(
198 | 'after',
199 | `
200 | Examples:
201 | $ tm tags rename old-name new-name
202 | `
203 | )
204 | .action(async (oldName, newName) => {
205 | await this.executeRename(oldName, newName);
206 | });
207 | }
208 |
209 | /**
210 | * Add copy subcommand
211 | */
212 | private addCopyCommand(): void {
213 | this.command('copy')
214 | .description('Copy a tag with all its tasks')
215 | .argument('<source>', 'Source tag name')
216 | .argument('<target>', 'Target tag name')
217 | .option('--description <desc>', 'Description for the new tag')
218 | .addHelpText(
219 | 'after',
220 | `
221 | Examples:
222 | $ tm tags copy sprint-1 sprint-2
223 | $ tm tags copy sprint-1 sprint-2 --description "Next sprint tasks"
224 | `
225 | )
226 | .action(async (source, target, options) => {
227 | await this.executeCopy(source, target, options);
228 | });
229 | }
230 |
231 | /**
232 | * Initialize TmCore if not already initialized
233 | * Required for bridge functions to work properly
234 | */
235 | private async initTmCore(): Promise<void> {
236 | if (!this.tmCore) {
237 | this.tmCore = await createTmCore({
238 | projectPath: process.cwd()
239 | });
240 | }
241 | }
242 |
243 | /**
244 | * Execute list tags
245 | */
246 | private async executeList(options?: {
247 | showMetadata?: boolean;
248 | }): Promise<void> {
249 | try {
250 | // Initialize tmCore first (needed by bridge functions)
251 | await this.initTmCore();
252 |
253 | const { projectRoot, tasksPath } = getProjectPaths();
254 |
255 | // Use legacy function which handles both API and file storage
256 | const listResult = (await legacyListTags(
257 | tasksPath,
258 | {
259 | showTaskCounts: true,
260 | showMetadata: options?.showMetadata || false
261 | },
262 | { projectRoot },
263 | 'text'
264 | )) as LegacyListTagsResult;
265 |
266 | this.setLastResult({
267 | success: true,
268 | action: 'list',
269 | tags: listResult.tags,
270 | currentTag: listResult.currentTag,
271 | message: `Found ${listResult.totalTags} tag(s)`
272 | });
273 | } catch (error: any) {
274 | displayError(error);
275 | this.setLastResult({
276 | success: false,
277 | action: 'list',
278 | message: error.message
279 | });
280 | this.handleError(
281 | error instanceof Error
282 | ? error
283 | : new Error(error.message || String(error))
284 | );
285 | }
286 | }
287 |
288 | /**
289 | * Execute add tag
290 | */
291 | private async executeAdd(
292 | name: string,
293 | options?: {
294 | description?: string;
295 | copyFrom?: string;
296 | fromBranch?: boolean;
297 | }
298 | ): Promise<void> {
299 | try {
300 | // Initialize tmCore first (needed by bridge functions)
301 | await this.initTmCore();
302 |
303 | const { projectRoot, tasksPath } = getProjectPaths();
304 |
305 | // Use legacy function which handles both API and file storage
306 | await legacyCreateTag(
307 | tasksPath,
308 | name,
309 | {
310 | description: options?.description,
311 | copyFromTag: options?.copyFrom,
312 | fromBranch: options?.fromBranch
313 | } as LegacyCreateTagOptions,
314 | { projectRoot },
315 | 'text'
316 | );
317 |
318 | this.setLastResult({
319 | success: true,
320 | action: 'add',
321 | message: `Created tag: ${name}`
322 | });
323 | } catch (error: any) {
324 | displayError(error);
325 | this.setLastResult({
326 | success: false,
327 | action: 'add',
328 | message: error.message
329 | });
330 | this.handleError(
331 | error instanceof Error
332 | ? error
333 | : new Error(error.message || String(error))
334 | );
335 | }
336 | }
337 |
338 | /**
339 | * Execute use/switch tag
340 | */
341 | private async executeUse(name: string): Promise<void> {
342 | try {
343 | // Initialize tmCore first (needed by bridge functions)
344 | await this.initTmCore();
345 |
346 | const { projectRoot, tasksPath } = getProjectPaths();
347 |
348 | // Use legacy function which handles both API and file storage
349 | const useResult = (await legacyUseTag(
350 | tasksPath,
351 | name,
352 | {},
353 | { projectRoot },
354 | 'text'
355 | )) as LegacyUseTagResult;
356 |
357 | this.setLastResult({
358 | success: true,
359 | action: 'use',
360 | currentTag: useResult.currentTag,
361 | message: `Switched to tag: ${name}`
362 | });
363 | } catch (error: any) {
364 | displayError(error);
365 | this.setLastResult({
366 | success: false,
367 | action: 'use',
368 | message: error.message
369 | });
370 | this.handleError(
371 | error instanceof Error
372 | ? error
373 | : new Error(error.message || String(error))
374 | );
375 | }
376 | }
377 |
378 | /**
379 | * Execute remove tag
380 | */
381 | private async executeRemove(
382 | name: string,
383 | options?: { yes?: boolean }
384 | ): Promise<void> {
385 | try {
386 | // Initialize tmCore first (needed by bridge functions)
387 | await this.initTmCore();
388 |
389 | const { projectRoot, tasksPath } = getProjectPaths();
390 |
391 | // Use legacy function which handles both API and file storage
392 | await legacyDeleteTag(
393 | tasksPath,
394 | name,
395 | { yes: options?.yes || false },
396 | { projectRoot },
397 | 'text'
398 | );
399 |
400 | this.setLastResult({
401 | success: true,
402 | action: 'remove',
403 | message: `Removed tag: ${name}`
404 | });
405 | } catch (error: any) {
406 | displayError(error);
407 | this.setLastResult({
408 | success: false,
409 | action: 'remove',
410 | message: error.message
411 | });
412 | this.handleError(
413 | error instanceof Error
414 | ? error
415 | : new Error(error.message || String(error))
416 | );
417 | }
418 | }
419 |
420 | /**
421 | * Execute rename tag
422 | */
423 | private async executeRename(oldName: string, newName: string): Promise<void> {
424 | try {
425 | // Initialize tmCore first (needed by bridge functions)
426 | await this.initTmCore();
427 |
428 | const { projectRoot, tasksPath } = getProjectPaths();
429 |
430 | // Use legacy function which handles both API and file storage
431 | await legacyRenameTag(
432 | tasksPath,
433 | oldName,
434 | newName,
435 | {},
436 | { projectRoot },
437 | 'text'
438 | );
439 |
440 | this.setLastResult({
441 | success: true,
442 | action: 'rename',
443 | message: `Renamed tag from "${oldName}" to "${newName}"`
444 | });
445 | } catch (error: any) {
446 | displayError(error);
447 | this.setLastResult({
448 | success: false,
449 | action: 'rename',
450 | message: error.message
451 | });
452 | this.handleError(
453 | error instanceof Error
454 | ? error
455 | : new Error(error.message || String(error))
456 | );
457 | }
458 | }
459 |
460 | /**
461 | * Execute copy tag
462 | */
463 | private async executeCopy(
464 | source: string,
465 | target: string,
466 | options?: { description?: string }
467 | ): Promise<void> {
468 | try {
469 | // Initialize tmCore first (needed by bridge functions)
470 | await this.initTmCore();
471 |
472 | const { projectRoot, tasksPath } = getProjectPaths();
473 |
474 | // Use legacy function which handles both API and file storage
475 | await legacyCopyTag(
476 | tasksPath,
477 | source,
478 | target,
479 | { description: options?.description },
480 | { projectRoot },
481 | 'text'
482 | );
483 |
484 | this.setLastResult({
485 | success: true,
486 | action: 'copy',
487 | message: `Copied tag from "${source}" to "${target}"`
488 | });
489 | } catch (error: any) {
490 | displayError(error);
491 | this.setLastResult({
492 | success: false,
493 | action: 'copy',
494 | message: error.message
495 | });
496 | this.handleError(
497 | error instanceof Error
498 | ? error
499 | : new Error(error.message || String(error))
500 | );
501 | }
502 | }
503 |
504 | /**
505 | * Set the last result for programmatic access
506 | */
507 | private setLastResult(result: TagsResult): void {
508 | this.lastResult = result;
509 | }
510 |
511 | /**
512 | * Get the last result (for programmatic usage)
513 | */
514 | getLastResult(): TagsResult | undefined {
515 | return this.lastResult;
516 | }
517 |
518 | /**
519 | * Enable throwing errors instead of process.exit for programmatic usage
520 | * @param shouldThrow If true, throws errors; if false, calls process.exit (default)
521 | */
522 | public setThrowOnError(shouldThrow: boolean): this {
523 | this.throwOnError = shouldThrow;
524 | return this;
525 | }
526 |
527 | /**
528 | * Handle error by either exiting or throwing based on throwOnError flag
529 | */
530 | private handleError(error: Error): never {
531 | if (this.throwOnError) {
532 | throw error;
533 | }
534 | process.exit(1);
535 | }
536 |
537 | /**
538 | * Register this command on an existing program
539 | */
540 | static register(program: Command, name?: string): TagsCommand {
541 | const tagsCommand = new TagsCommand(name);
542 | program.addCommand(tagsCommand);
543 | return tagsCommand;
544 | }
545 | }
546 |
```
--------------------------------------------------------------------------------
/apps/docs/capabilities/index.mdx:
--------------------------------------------------------------------------------
```markdown
1 | ---
2 | title: Technical Capabilities
3 | sidebarTitle: "Technical Capabilities"
4 | ---
5 |
6 | # Capabilities (Technical)
7 |
8 | Discover the technical capabilities of Task Master, including supported models, integrations, and more.
9 |
10 | # CLI Interface Synopsis
11 |
12 | This document outlines the command-line interface (CLI) for the Task Master application, as defined in `bin/task-master.js` and the `scripts/modules/commands.js` file (which I will assume exists based on the context). This guide is intended for those writing user-facing documentation to understand how users interact with the application from the command line.
13 |
14 | ## Entry Point
15 |
16 | The main entry point for the CLI is the `task-master` command, which is an executable script that spawns the main application logic in `scripts/dev.js`.
17 |
18 | ## Global Options
19 |
20 | The following options are available for all commands:
21 |
22 | - `-h, --help`: Display help information.
23 | - `--version`: Display the application's version.
24 |
25 | ## Commands
26 |
27 | The CLI is organized into a series of commands, each with its own set of options. The following is a summary of the available commands, categorized by their functionality.
28 |
29 | ### 1. Task and Subtask Management
30 |
31 | - **`add`**: Creates a new task using an AI-powered prompt.
32 | - `--prompt <prompt>`: The prompt to use for generating the task.
33 | - `--dependencies <dependencies>`: A comma-separated list of task IDs that this task depends on.
34 | - `--priority <priority>`: The priority of the task (e.g., `high`, `medium`, `low`).
35 | - **`add-subtask`**: Adds a subtask to a parent task.
36 | - `--parent-id <parentId>`: The ID of the parent task.
37 | - `--task-id <taskId>`: The ID of an existing task to convert to a subtask.
38 | - `--title <title>`: The title of the new subtask.
39 | - **`remove`**: Removes one or more tasks or subtasks.
40 | - `--ids <ids>`: A comma-separated list of task or subtask IDs to remove.
41 | - **`remove-subtask`**: Removes a subtask from its parent.
42 | - `--id <subtaskId>`: The ID of the subtask to remove (in the format `parentId.subtaskId`).
43 | - `--convert-to-task`: Converts the subtask to a standalone task.
44 | - **`update`**: Updates multiple tasks starting from a specific ID.
45 | - `--from <fromId>`: The ID of the task to start updating from.
46 | - `--prompt <prompt>`: The new context to apply to the tasks.
47 | - **`update-task`**: Updates a single task.
48 | - `--id <taskId>`: The ID of the task to update.
49 | - `--prompt <prompt>`: The new context to apply to the task.
50 | - **`update-subtask`**: Appends information to a subtask.
51 | - `--id <subtaskId>`: The ID of the subtask to update (in the format `parentId.subtaskId`).
52 | - `--prompt <prompt>`: The information to append to the subtask.
53 | - **`move`**: Moves a task or subtask.
54 | - `--from <sourceId>`: The ID of the task or subtask to move.
55 | - `--to <destinationId>`: The destination ID.
56 | - **`clear-subtasks`**: Clears all subtasks from one or more tasks.
57 | - `--ids <ids>`: A comma-separated list of task IDs.
58 |
59 | ### 2. Task Information and Status
60 |
61 | - **`list`**: Lists all tasks.
62 | - `--status <status>`: Filters tasks by status.
63 | - `--with-subtasks`: Includes subtasks in the list.
64 | - **`show`**: Shows the details of a specific task.
65 | - `--id <taskId>`: The ID of the task to show.
66 | - **`next`**: Shows the next task to work on.
67 | - **`set-status`**: Sets the status of a task or subtask.
68 | - `--id <id>`: The ID of the task or subtask.
69 | - `--status <status>`: The new status.
70 |
71 | ### 3. Task Analysis and Expansion
72 |
73 | - **`parse-prd`**: Parses a PRD to generate tasks.
74 | - `--file <file>`: The path to the PRD file.
75 | - `--num-tasks <numTasks>`: The number of tasks to generate.
76 | - **`expand`**: Expands a task into subtasks.
77 | - `--id <taskId>`: The ID of the task to expand.
78 | - `--num-subtasks <numSubtasks>`: The number of subtasks to generate.
79 | - **`expand-all`**: Expands all eligible tasks.
80 | - `--num-subtasks <numSubtasks>`: The number of subtasks to generate for each task.
81 | - **`analyze-complexity`**: Analyzes task complexity.
82 | - `--file <file>`: The path to the tasks file.
83 | - **`complexity-report`**: Displays the complexity analysis report.
84 |
85 | ### 4. Project and Configuration
86 |
87 | - **`init`**: Initializes a new project.
88 | - **`generate`**: Generates individual task files.
89 | - **`migrate`**: Migrates a project to the new directory structure.
90 | - **`research`**: Performs AI-powered research.
91 | - `--query <query>`: The research query.
92 |
93 | This synopsis provides a comprehensive overview of the CLI commands and their options, which should be helpful for creating user-facing documentation.
94 |
95 |
96 | # Core Implementation Synopsis
97 |
98 | This document provides a high-level overview of the core implementation of the Task Master application, focusing on the functionalities exposed through `scripts/modules/task-manager.js`. This serves as a guide for understanding the application's capabilities when writing user-facing documentation.
99 |
100 | ## Core Concepts
101 |
102 | The application revolves around the management of tasks and subtasks, which are stored in a `tasks.json` file. The core logic provides functionalities to create, read, update, and delete tasks and subtasks, as well as manage their dependencies and statuses.
103 |
104 | ### Task Structure
105 |
106 | A task is a JSON object with the following key properties:
107 |
108 | - `id`: A unique number identifying the task.
109 | - `title`: A string representing the task's title.
110 | - `description`: A string providing a brief description of the task.
111 | - `details`: A string containing detailed information about the task.
112 | - `testStrategy`: A string describing how to test the task.
113 | - `status`: A string representing the task's current status (e.g., `pending`, `in-progress`, `done`).
114 | - `dependencies`: An array of task IDs that this task depends on.
115 | - `priority`: A string representing the task's priority (e.g., `high`, `medium`, `low`).
116 | - `subtasks`: An array of subtask objects.
117 |
118 | A subtask has a similar structure to a task but is nested within a parent task.
119 |
120 | ## Feature Categories
121 |
122 | The core functionalities can be categorized as follows:
123 |
124 | ### 1. Task and Subtask Management
125 |
126 | These functions are the bread and butter of the application, allowing for the creation, modification, and deletion of tasks and subtasks.
127 |
128 | - **`addTask(prompt, dependencies, priority)`**: Creates a new task using an AI-powered prompt to generate the title, description, details, and test strategy. It can also be used to create a task manually by providing the task data directly.
129 | - **`addSubtask(parentId, existingTaskId, newSubtaskData)`**: Adds a subtask to a parent task. It can either convert an existing task into a subtask or create a new subtask from scratch.
130 | - **`removeTask(taskIds)`**: Removes one or more tasks or subtasks.
131 | - **`removeSubtask(subtaskId, convertToTask)`**: Removes a subtask from its parent. It can optionally convert the subtask into a standalone task.
132 | - **`updateTaskById(taskId, prompt)`**: Updates a task's information based on a prompt.
133 | - **`updateSubtaskById(subtaskId, prompt)`**: Appends additional information to a subtask's details.
134 | - **`updateTasks(fromId, prompt)`**: Updates multiple tasks starting from a specific ID based on a new context.
135 | - **`moveTask(sourceId, destinationId)`**: Moves a task or subtask to a new position.
136 | - **`clearSubtasks(taskIds)`**: Clears all subtasks from one or more tasks.
137 |
138 | ### 2. Task Information and Status
139 |
140 | These functions are used to retrieve information about tasks and manage their status.
141 |
142 | - **`listTasks(statusFilter, withSubtasks)`**: Lists all tasks, with options to filter by status and include subtasks.
143 | - **`findTaskById(taskId)`**: Finds a task by its ID.
144 | - **`taskExists(taskId)`**: Checks if a task with a given ID exists.
145 | - **`setTaskStatus(taskIdInput, newStatus)`**: Sets the status of a task or subtask.
146 | -al
147 | - **`updateSingleTaskStatus(taskIdInput, newStatus)`**: A helper function to update the status of a single task or subtask.
148 | - **`findNextTask()`**: Determines the next task to work on based on dependencies and status.
149 |
150 | ### 3. Task Analysis and Expansion
151 |
152 | These functions leverage AI to analyze and break down tasks.
153 |
154 | - **`parsePRD(prdPath, numTasks)`**: Parses a Product Requirements Document (PRD) to generate an initial set of tasks.
155 | - **`expandTask(taskId, numSubtasks)`**: Expands a task into a specified number of subtasks using AI.
156 | - **`expandAllTasks(numSubtasks)`**: Expands all eligible pending or in-progress tasks.
157 | - **`analyzeTaskComplexity(options)`**: Analyzes the complexity of tasks and generates recommendations for expansion.
158 | - **`readComplexityReport()`**: Reads the complexity analysis report.
159 |
160 | ### 4. Dependency Management
161 |
162 | These functions are crucial for managing the relationships between tasks.
163 |
164 | - **`isTaskDependentOn(task, targetTaskId)`**: Checks if a task has a direct or indirect dependency on another task.
165 |
166 | ### 5. Project and Configuration
167 |
168 | These functions are for managing the project and its configuration.
169 |
170 | - **`generateTaskFiles()`**: Generates individual task files from `tasks.json`.
171 | - **`migrateProject()`**: Migrates the project to the new `.taskmaster` directory structure.
172 | - **`performResearch(query, options)`**: Performs AI-powered research with project context.
173 |
174 | This overview should provide a solid foundation for creating user-facing documentation. For more detailed information on each function, refer to the source code in `scripts/modules/task-manager/`.
175 |
176 |
177 | # MCP Interface Synopsis
178 |
179 | This document provides an overview of the MCP (Machine-to-Machine Communication Protocol) interface for the Task Master application. The MCP interface is defined in the `mcp-server/` directory and exposes the application's core functionalities as a set of tools that can be called remotely.
180 |
181 | ## Core Concepts
182 |
183 | The MCP interface is built on top of the `fastmcp` library and registers a set of tools that correspond to the core functionalities of the Task Master application. These tools are defined in the `mcp-server/src/tools/` directory and are registered with the MCP server in `mcp-server/src/tools/index.js`.
184 |
185 | Each tool is defined with a name, a description, and a set of parameters that are validated using the `zod` library. The `execute` function of each tool calls the corresponding core logic function from `scripts/modules/task-manager.js`.
186 |
187 | ## Tool Categories
188 |
189 | The MCP tools can be categorized in the same way as the core functionalities:
190 |
191 | ### 1. Task and Subtask Management
192 |
193 | - **`add_task`**: Creates a new task.
194 | - **`add_subtask`**: Adds a subtask to a parent task.
195 | - **`remove_task`**: Removes one or more tasks or subtasks.
196 | - **`remove_subtask`**: Removes a subtask from its parent.
197 | - **`update_task`**: Updates a single task.
198 | - **`update_subtask`**: Appends information to a subtask.
199 | - **`update`**: Updates multiple tasks.
200 | - **`move_task`**: Moves a task or subtask.
201 | - **`clear_subtasks`**: Clears all subtasks from one or more tasks.
202 |
203 | ### 2. Task Information and Status
204 |
205 | - **`get_tasks`**: Lists all tasks.
206 | - **`get_task`**: Shows the details of a specific task.
207 | - **`next_task`**: Shows the next task to work on.
208 | - **`set_task_status`**: Sets the status of a task or subtask.
209 |
210 | ### 3. Task Analysis and Expansion
211 |
212 | - **`parse_prd`**: Parses a PRD to generate tasks.
213 | - **`expand_task`**: Expands a task into subtasks.
214 | - **`expand_all`**: Expands all eligible tasks.
215 | - **`analyze_project_complexity`**: Analyzes task complexity.
216 | - **`complexity_report`**: Displays the complexity analysis report.
217 |
218 | ### 4. Dependency Management
219 |
220 | - **`add_dependency`**: Adds a dependency to a task.
221 | - **`remove_dependency`**: Removes a dependency from a task.
222 | - **`validate_dependencies`**: Validates the dependencies of all tasks.
223 | - **`fix_dependencies`**: Fixes any invalid dependencies.
224 |
225 | ### 5. Project and Configuration
226 |
227 | - **`initialize_project`**: Initializes a new project.
228 | - **`generate`**: Generates individual task files.
229 | - **`models`**: Manages AI model configurations.
230 | - **`research`**: Performs AI-powered research.
231 |
232 | ### 6. Tag Management
233 |
234 | - **`add_tag`**: Creates a new tag.
235 | - **`delete_tag`**: Deletes a tag.
236 | - **`list_tags`**: Lists all tags.
237 | - **`use_tag`**: Switches to a different tag.
238 | - **`rename_tag`**: Renames a tag.
239 | - **`copy_tag`**: Copies a tag.
240 |
241 | This synopsis provides a clear overview of the MCP interface and its available tools, which will be valuable for anyone writing documentation for developers who need to interact with the Task Master application programmatically.
```