#
tokens: 49174/50000 8/975 files (page 34/69)
lines: on (toggle) GitHub
raw markdown copy reset
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.
```
Page 34/69FirstPrevNextLast