#
tokens: 47595/50000 2/975 files (page 59/69)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 59 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

--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/parse-prd.test.js:
--------------------------------------------------------------------------------

```javascript
   1 | /**
   2 |  * Tests for the parse-prd.js module
   3 |  */
   4 | import { jest } from '@jest/globals';
   5 | 
   6 | // Mock the dependencies before importing the module under test
   7 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
   8 | 	readJSON: jest.fn(),
   9 | 	writeJSON: jest.fn(),
  10 | 	log: jest.fn(),
  11 | 	CONFIG: {
  12 | 		model: 'mock-claude-model',
  13 | 		maxTokens: 4000,
  14 | 		temperature: 0.7,
  15 | 		debug: false
  16 | 	},
  17 | 	sanitizePrompt: jest.fn((prompt) => prompt),
  18 | 	truncate: jest.fn((text) => text),
  19 | 	isSilentMode: jest.fn(() => false),
  20 | 	enableSilentMode: jest.fn(),
  21 | 	disableSilentMode: jest.fn(),
  22 | 	findTaskById: jest.fn(),
  23 | 	ensureTagMetadata: jest.fn((tagObj) => tagObj),
  24 | 	getCurrentTag: jest.fn(() => 'master'),
  25 | 	promptYesNo: jest.fn()
  26 | }));
  27 | 
  28 | jest.unstable_mockModule(
  29 | 	'../../../../../scripts/modules/ai-services-unified.js',
  30 | 	() => ({
  31 | 		generateObjectService: jest.fn().mockResolvedValue({
  32 | 			tasks: [
  33 | 				{
  34 | 					id: 1,
  35 | 					title: 'Test Task 1',
  36 | 					priority: 'high',
  37 | 					description: 'Test description 1',
  38 | 					status: 'pending',
  39 | 					dependencies: []
  40 | 				},
  41 | 				{
  42 | 					id: 2,
  43 | 					title: 'Test Task 2',
  44 | 					priority: 'medium',
  45 | 					description: 'Test description 2',
  46 | 					status: 'pending',
  47 | 					dependencies: []
  48 | 				},
  49 | 				{
  50 | 					id: 3,
  51 | 					title: 'Test Task 3',
  52 | 					priority: 'low',
  53 | 					description: 'Test description 3',
  54 | 					status: 'pending',
  55 | 					dependencies: []
  56 | 				}
  57 | 			]
  58 | 		}),
  59 | 		streamObjectService: jest.fn().mockImplementation(async () => {
  60 | 			// Return an object with partialObjectStream as a getter that returns the async generator
  61 | 			return {
  62 | 				mainResult: {
  63 | 					get partialObjectStream() {
  64 | 						return (async function* () {
  65 | 							yield { tasks: [] };
  66 | 							yield {
  67 | 								tasks: [
  68 | 									{
  69 | 										id: 1,
  70 | 										title: 'Test Task 1',
  71 | 										priority: 'high',
  72 | 										description: 'Test description 1',
  73 | 										status: 'pending',
  74 | 										dependencies: []
  75 | 									}
  76 | 								]
  77 | 							};
  78 | 							yield {
  79 | 								tasks: [
  80 | 									{
  81 | 										id: 1,
  82 | 										title: 'Test Task 1',
  83 | 										priority: 'high',
  84 | 										description: 'Test description 1',
  85 | 										status: 'pending',
  86 | 										dependencies: []
  87 | 									},
  88 | 									{
  89 | 										id: 2,
  90 | 										title: 'Test Task 2',
  91 | 										priority: 'medium',
  92 | 										description: 'Test description 2',
  93 | 										status: 'pending',
  94 | 										dependencies: []
  95 | 									}
  96 | 								]
  97 | 							};
  98 | 							yield {
  99 | 								tasks: [
 100 | 									{
 101 | 										id: 1,
 102 | 										title: 'Test Task 1',
 103 | 										priority: 'high',
 104 | 										description: 'Test description 1',
 105 | 										status: 'pending',
 106 | 										dependencies: []
 107 | 									},
 108 | 									{
 109 | 										id: 2,
 110 | 										title: 'Test Task 2',
 111 | 										priority: 'medium',
 112 | 										description: 'Test description 2',
 113 | 										status: 'pending',
 114 | 										dependencies: []
 115 | 									},
 116 | 									{
 117 | 										id: 3,
 118 | 										title: 'Test Task 3',
 119 | 										priority: 'low',
 120 | 										description: 'Test description 3',
 121 | 										status: 'pending',
 122 | 										dependencies: []
 123 | 									}
 124 | 								]
 125 | 							};
 126 | 						})();
 127 | 					},
 128 | 					usage: Promise.resolve({
 129 | 						promptTokens: 100,
 130 | 						completionTokens: 200,
 131 | 						totalTokens: 300
 132 | 					}),
 133 | 					object: Promise.resolve({
 134 | 						tasks: [
 135 | 							{
 136 | 								id: 1,
 137 | 								title: 'Test Task 1',
 138 | 								priority: 'high',
 139 | 								description: 'Test description 1',
 140 | 								status: 'pending',
 141 | 								dependencies: []
 142 | 							},
 143 | 							{
 144 | 								id: 2,
 145 | 								title: 'Test Task 2',
 146 | 								priority: 'medium',
 147 | 								description: 'Test description 2',
 148 | 								status: 'pending',
 149 | 								dependencies: []
 150 | 							},
 151 | 							{
 152 | 								id: 3,
 153 | 								title: 'Test Task 3',
 154 | 								priority: 'low',
 155 | 								description: 'Test description 3',
 156 | 								status: 'pending',
 157 | 								dependencies: []
 158 | 							}
 159 | 						]
 160 | 					})
 161 | 				},
 162 | 				providerName: 'anthropic',
 163 | 				modelId: 'claude-3-5-sonnet-20241022',
 164 | 				telemetryData: {}
 165 | 			};
 166 | 		})
 167 | 	})
 168 | );
 169 | 
 170 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
 171 | 	getStatusWithColor: jest.fn((status) => status),
 172 | 	startLoadingIndicator: jest.fn(),
 173 | 	stopLoadingIndicator: jest.fn(),
 174 | 	displayAiUsageSummary: jest.fn()
 175 | }));
 176 | 
 177 | jest.unstable_mockModule(
 178 | 	'../../../../../scripts/modules/config-manager.js',
 179 | 	() => ({
 180 | 		getDebugFlag: jest.fn(() => false),
 181 | 		getMainModelId: jest.fn(() => 'claude-3-5-sonnet'),
 182 | 		getResearchModelId: jest.fn(() => 'claude-3-5-sonnet'),
 183 | 		getParametersForRole: jest.fn(() => ({
 184 | 			provider: 'anthropic',
 185 | 			modelId: 'claude-3-5-sonnet'
 186 | 		})),
 187 | 		getDefaultNumTasks: jest.fn(() => 10),
 188 | 		getDefaultPriority: jest.fn(() => 'medium'),
 189 | 		getMainProvider: jest.fn(() => 'openai'),
 190 | 		getResearchProvider: jest.fn(() => 'perplexity'),
 191 | 		hasCodebaseAnalysis: jest.fn(() => false)
 192 | 	})
 193 | );
 194 | 
 195 | jest.unstable_mockModule(
 196 | 	'../../../../../scripts/modules/task-manager/generate-task-files.js',
 197 | 	() => ({
 198 | 		default: jest.fn().mockResolvedValue()
 199 | 	})
 200 | );
 201 | 
 202 | jest.unstable_mockModule(
 203 | 	'../../../../../scripts/modules/task-manager/models.js',
 204 | 	() => ({
 205 | 		getModelConfiguration: jest.fn(() => ({
 206 | 			model: 'mock-model',
 207 | 			maxTokens: 4000,
 208 | 			temperature: 0.7
 209 | 		}))
 210 | 	})
 211 | );
 212 | 
 213 | jest.unstable_mockModule(
 214 | 	'../../../../../scripts/modules/prompt-manager.js',
 215 | 	() => ({
 216 | 		getPromptManager: jest.fn().mockReturnValue({
 217 | 			loadPrompt: jest.fn().mockImplementation((templateName, params) => {
 218 | 				// Create dynamic mock prompts based on the parameters
 219 | 				const { numTasks } = params || {};
 220 | 				let numTasksText = '';
 221 | 
 222 | 				if (numTasks > 0) {
 223 | 					numTasksText = `approximately ${numTasks}`;
 224 | 				} else {
 225 | 					numTasksText = 'an appropriate number of';
 226 | 				}
 227 | 
 228 | 				return Promise.resolve({
 229 | 					systemPrompt: 'Mocked system prompt for parse-prd',
 230 | 					userPrompt: `Generate ${numTasksText} top-level development tasks from the PRD content.`
 231 | 				});
 232 | 			})
 233 | 		})
 234 | 	})
 235 | );
 236 | 
 237 | // Mock fs module
 238 | jest.unstable_mockModule('fs', () => ({
 239 | 	default: {
 240 | 		readFileSync: jest.fn(),
 241 | 		existsSync: jest.fn(),
 242 | 		mkdirSync: jest.fn(),
 243 | 		writeFileSync: jest.fn(),
 244 | 		promises: {
 245 | 			readFile: jest.fn()
 246 | 		}
 247 | 	},
 248 | 	readFileSync: jest.fn(),
 249 | 	existsSync: jest.fn(),
 250 | 	mkdirSync: jest.fn(),
 251 | 	writeFileSync: jest.fn()
 252 | }));
 253 | 
 254 | // Mock path module
 255 | jest.unstable_mockModule('path', () => ({
 256 | 	default: {
 257 | 		dirname: jest.fn(),
 258 | 		join: jest.fn((dir, file) => `${dir}/${file}`)
 259 | 	},
 260 | 	dirname: jest.fn(),
 261 | 	join: jest.fn((dir, file) => `${dir}/${file}`)
 262 | }));
 263 | 
 264 | // Mock JSONParser for streaming tests
 265 | jest.unstable_mockModule('@streamparser/json', () => ({
 266 | 	JSONParser: jest.fn().mockImplementation(() => ({
 267 | 		onValue: jest.fn(),
 268 | 		onError: jest.fn(),
 269 | 		write: jest.fn(),
 270 | 		end: jest.fn()
 271 | 	}))
 272 | }));
 273 | 
 274 | // Mock stream-parser functions
 275 | jest.unstable_mockModule('../../../../../src/utils/stream-parser.js', () => {
 276 | 	// Define mock StreamingError class
 277 | 	class StreamingError extends Error {
 278 | 		constructor(message, code) {
 279 | 			super(message);
 280 | 			this.name = 'StreamingError';
 281 | 			this.code = code;
 282 | 		}
 283 | 	}
 284 | 
 285 | 	// Define mock error codes
 286 | 	const STREAMING_ERROR_CODES = {
 287 | 		NOT_ASYNC_ITERABLE: 'STREAMING_NOT_SUPPORTED',
 288 | 		STREAM_PROCESSING_FAILED: 'STREAM_PROCESSING_FAILED',
 289 | 		STREAM_NOT_ITERABLE: 'STREAM_NOT_ITERABLE'
 290 | 	};
 291 | 
 292 | 	return {
 293 | 		parseStream: jest.fn().mockResolvedValue({
 294 | 			items: [{ id: 1, title: 'Test Task', priority: 'high' }],
 295 | 			accumulatedText:
 296 | 				'{"tasks":[{"id":1,"title":"Test Task","priority":"high"}]}',
 297 | 			estimatedTokens: 50,
 298 | 			usedFallback: false
 299 | 		}),
 300 | 		createTaskProgressCallback: jest.fn().mockReturnValue(jest.fn()),
 301 | 		createConsoleProgressCallback: jest.fn().mockReturnValue(jest.fn()),
 302 | 		StreamingError,
 303 | 		STREAMING_ERROR_CODES
 304 | 	};
 305 | });
 306 | 
 307 | // Mock progress tracker to prevent intervals
 308 | jest.unstable_mockModule(
 309 | 	'../../../../../src/progress/parse-prd-tracker.js',
 310 | 	() => ({
 311 | 		createParsePrdTracker: jest.fn().mockReturnValue({
 312 | 			start: jest.fn(),
 313 | 			stop: jest.fn(),
 314 | 			cleanup: jest.fn(),
 315 | 			updateTokens: jest.fn(),
 316 | 			addTaskLine: jest.fn(),
 317 | 			trackTaskPriority: jest.fn(),
 318 | 			getSummary: jest.fn().mockReturnValue({
 319 | 				taskPriorities: { high: 0, medium: 0, low: 0 },
 320 | 				elapsedTime: 0,
 321 | 				actionVerb: 'generated'
 322 | 			})
 323 | 		})
 324 | 	})
 325 | );
 326 | 
 327 | // Mock UI functions to prevent any display delays
 328 | jest.unstable_mockModule('../../../../../src/ui/parse-prd.js', () => ({
 329 | 	displayParsePrdStart: jest.fn(),
 330 | 	displayParsePrdSummary: jest.fn()
 331 | }));
 332 | 
 333 | // Import the mocked modules
 334 | const { readJSON, promptYesNo } = await import(
 335 | 	'../../../../../scripts/modules/utils.js'
 336 | );
 337 | 
 338 | const { generateObjectService, streamObjectService } = await import(
 339 | 	'../../../../../scripts/modules/ai-services-unified.js'
 340 | );
 341 | 
 342 | const { JSONParser } = await import('@streamparser/json');
 343 | 
 344 | const { parseStream, StreamingError, STREAMING_ERROR_CODES } = await import(
 345 | 	'../../../../../src/utils/stream-parser.js'
 346 | );
 347 | 
 348 | const { createParsePrdTracker } = await import(
 349 | 	'../../../../../src/progress/parse-prd-tracker.js'
 350 | );
 351 | 
 352 | const { displayParsePrdStart, displayParsePrdSummary } = await import(
 353 | 	'../../../../../src/ui/parse-prd.js'
 354 | );
 355 | 
 356 | // Note: getDefaultNumTasks validation happens at CLI/MCP level, not in the main parse-prd module
 357 | const generateTaskFiles = (
 358 | 	await import(
 359 | 		'../../../../../scripts/modules/task-manager/generate-task-files.js'
 360 | 	)
 361 | ).default;
 362 | 
 363 | const fs = await import('fs');
 364 | const path = await import('path');
 365 | 
 366 | // Import the module under test
 367 | const { default: parsePRD } = await import(
 368 | 	'../../../../../scripts/modules/task-manager/parse-prd/parse-prd.js'
 369 | );
 370 | 
 371 | // Sample data for tests (from main test file)
 372 | const sampleClaudeResponse = {
 373 | 	tasks: [
 374 | 		{
 375 | 			id: 1,
 376 | 			title: 'Setup Project Structure',
 377 | 			description: 'Initialize the project with necessary files and folders',
 378 | 			status: 'pending',
 379 | 			dependencies: [],
 380 | 			priority: 'high'
 381 | 		},
 382 | 		{
 383 | 			id: 2,
 384 | 			title: 'Implement Core Features',
 385 | 			description: 'Build the main functionality',
 386 | 			status: 'pending',
 387 | 			dependencies: [1],
 388 | 			priority: 'high'
 389 | 		}
 390 | 	],
 391 | 	metadata: {
 392 | 		projectName: 'Test Project',
 393 | 		totalTasks: 2,
 394 | 		sourceFile: 'path/to/prd.txt',
 395 | 		generatedAt: expect.any(String)
 396 | 	}
 397 | };
 398 | 
 399 | describe('parsePRD', () => {
 400 | 	// Mock the sample PRD content
 401 | 	const samplePRDContent = '# Sample PRD for Testing';
 402 | 
 403 | 	// Mock existing tasks for append test - TAGGED FORMAT
 404 | 	const existingTasksData = {
 405 | 		master: {
 406 | 			tasks: [
 407 | 				{ id: 1, title: 'Existing Task 1', status: 'done' },
 408 | 				{ id: 2, title: 'Existing Task 2', status: 'pending' }
 409 | 			]
 410 | 		}
 411 | 	};
 412 | 
 413 | 	// Mock new tasks with continuing IDs for append test
 414 | 	const newTasksClaudeResponse = {
 415 | 		tasks: [
 416 | 			{ id: 3, title: 'New Task 3' },
 417 | 			{ id: 4, title: 'New Task 4' }
 418 | 		],
 419 | 		metadata: {
 420 | 			projectName: 'Test Project',
 421 | 			totalTasks: 2,
 422 | 			sourceFile: 'path/to/prd.txt',
 423 | 			generatedAt: expect.any(String)
 424 | 		}
 425 | 	};
 426 | 
 427 | 	beforeEach(() => {
 428 | 		// Reset all mocks
 429 | 		jest.clearAllMocks();
 430 | 
 431 | 		// Set up mocks for fs, path and other modules
 432 | 		fs.default.readFileSync.mockReturnValue(samplePRDContent);
 433 | 		fs.default.promises.readFile.mockResolvedValue(samplePRDContent);
 434 | 		fs.default.existsSync.mockReturnValue(true);
 435 | 		path.default.dirname.mockReturnValue('tasks');
 436 | 		generateObjectService.mockResolvedValue({
 437 | 			mainResult: sampleClaudeResponse,
 438 | 			telemetryData: {}
 439 | 		});
 440 | 		// Reset streamObjectService mock to working implementation
 441 | 		streamObjectService.mockImplementation(async () => {
 442 | 			return {
 443 | 				mainResult: {
 444 | 					get partialObjectStream() {
 445 | 						return (async function* () {
 446 | 							yield { tasks: [] };
 447 | 							yield { tasks: [sampleClaudeResponse.tasks[0]] };
 448 | 							yield {
 449 | 								tasks: [
 450 | 									sampleClaudeResponse.tasks[0],
 451 | 									sampleClaudeResponse.tasks[1]
 452 | 								]
 453 | 							};
 454 | 							yield sampleClaudeResponse;
 455 | 						})();
 456 | 					},
 457 | 					usage: Promise.resolve({
 458 | 						promptTokens: 100,
 459 | 						completionTokens: 200,
 460 | 						totalTokens: 300
 461 | 					}),
 462 | 					object: Promise.resolve(sampleClaudeResponse)
 463 | 				},
 464 | 				providerName: 'anthropic',
 465 | 				modelId: 'claude-3-5-sonnet-20241022',
 466 | 				telemetryData: {}
 467 | 			};
 468 | 		});
 469 | 		// generateTaskFiles.mockResolvedValue(undefined);
 470 | 		promptYesNo.mockResolvedValue(true); // Default to "yes" for confirmation
 471 | 
 472 | 		// Mock process.exit to prevent actual exit and throw error instead for CLI tests
 473 | 		jest.spyOn(process, 'exit').mockImplementation((code) => {
 474 | 			throw new Error(`process.exit was called with code ${code}`);
 475 | 		});
 476 | 
 477 | 		// Mock console.error to prevent output
 478 | 		jest.spyOn(console, 'error').mockImplementation(() => {});
 479 | 		jest.spyOn(console, 'log').mockImplementation(() => {});
 480 | 	});
 481 | 
 482 | 	afterEach(() => {
 483 | 		// Restore all mocks after each test
 484 | 		jest.restoreAllMocks();
 485 | 	});
 486 | 
 487 | 	test('should parse a PRD file and generate tasks', async () => {
 488 | 		// Setup mocks to simulate normal conditions (no existing output file)
 489 | 		fs.default.existsSync.mockImplementation((p) => {
 490 | 			if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
 491 | 			if (p === 'tasks') return true; // Directory exists
 492 | 			return false;
 493 | 		});
 494 | 
 495 | 		// Also mock the other fs methods that might be called
 496 | 		fs.default.readFileSync.mockReturnValue(samplePRDContent);
 497 | 		fs.default.promises.readFile.mockResolvedValue(samplePRDContent);
 498 | 
 499 | 		// Call the function with mcpLog to force non-streaming mode
 500 | 		const result = await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 501 | 			tag: 'master',
 502 | 			mcpLog: {
 503 | 				info: jest.fn(),
 504 | 				warn: jest.fn(),
 505 | 				error: jest.fn(),
 506 | 				debug: jest.fn(),
 507 | 				success: jest.fn()
 508 | 			}
 509 | 		});
 510 | 
 511 | 		// Verify fs.readFileSync was called with the correct arguments
 512 | 		expect(fs.default.readFileSync).toHaveBeenCalledWith(
 513 | 			'path/to/prd.txt',
 514 | 			'utf8'
 515 | 		);
 516 | 
 517 | 		// Verify generateObjectService was called
 518 | 		expect(generateObjectService).toHaveBeenCalled();
 519 | 
 520 | 		// Verify directory check
 521 | 		expect(fs.default.existsSync).toHaveBeenCalledWith('tasks');
 522 | 
 523 | 		// Verify fs.writeFileSync was called with the correct arguments in tagged format
 524 | 		expect(fs.default.writeFileSync).toHaveBeenCalledWith(
 525 | 			'tasks/tasks.json',
 526 | 			expect.stringContaining('"master"')
 527 | 		);
 528 | 
 529 | 		// Verify result
 530 | 		expect(result).toEqual({
 531 | 			success: true,
 532 | 			tasksPath: 'tasks/tasks.json',
 533 | 			telemetryData: {}
 534 | 		});
 535 | 
 536 | 		// Verify that the written data contains 2 tasks from sampleClaudeResponse in the correct tag
 537 | 		const writtenDataString = fs.default.writeFileSync.mock.calls[0][1];
 538 | 		const writtenData = JSON.parse(writtenDataString);
 539 | 		expect(writtenData.master.tasks.length).toBe(2);
 540 | 	});
 541 | 
 542 | 	test('should create the tasks directory if it does not exist', async () => {
 543 | 		// Mock existsSync to return false specifically for the directory check
 544 | 		// but true for the output file check (so we don't trigger confirmation path)
 545 | 		fs.default.existsSync.mockImplementation((p) => {
 546 | 			if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
 547 | 			if (p === 'tasks') return false; // Directory doesn't exist
 548 | 			return true; // Default for other paths
 549 | 		});
 550 | 
 551 | 		// Call the function with mcpLog to force non-streaming mode
 552 | 		await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 553 | 			tag: 'master',
 554 | 			mcpLog: {
 555 | 				info: jest.fn(),
 556 | 				warn: jest.fn(),
 557 | 				error: jest.fn(),
 558 | 				debug: jest.fn(),
 559 | 				success: jest.fn()
 560 | 			}
 561 | 		});
 562 | 
 563 | 		// Verify mkdir was called
 564 | 		expect(fs.default.mkdirSync).toHaveBeenCalledWith('tasks', {
 565 | 			recursive: true
 566 | 		});
 567 | 	});
 568 | 
 569 | 	test('should handle errors in the PRD parsing process', async () => {
 570 | 		// Mock an error in generateObjectService
 571 | 		const testError = new Error('Test error in AI API call');
 572 | 		generateObjectService.mockRejectedValueOnce(testError);
 573 | 
 574 | 		// Setup mocks to simulate normal file conditions (no existing file)
 575 | 		fs.default.existsSync.mockImplementation((p) => {
 576 | 			if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
 577 | 			if (p === 'tasks') return true; // Directory exists
 578 | 			return false;
 579 | 		});
 580 | 
 581 | 		// Call the function with mcpLog to make it think it's in MCP mode (which throws instead of process.exit)
 582 | 		await expect(
 583 | 			parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 584 | 				tag: 'master',
 585 | 				mcpLog: {
 586 | 					info: jest.fn(),
 587 | 					warn: jest.fn(),
 588 | 					error: jest.fn(),
 589 | 					debug: jest.fn(),
 590 | 					success: jest.fn()
 591 | 				}
 592 | 			})
 593 | 		).rejects.toThrow('Test error in AI API call');
 594 | 	});
 595 | 
 596 | 	test('should generate individual task files after creating tasks.json', async () => {
 597 | 		// Setup mocks to simulate normal conditions (no existing output file)
 598 | 		fs.default.existsSync.mockImplementation((p) => {
 599 | 			if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
 600 | 			if (p === 'tasks') return true; // Directory exists
 601 | 			return false;
 602 | 		});
 603 | 
 604 | 		// Call the function with mcpLog to force non-streaming mode
 605 | 		await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 606 | 			tag: 'master',
 607 | 			mcpLog: {
 608 | 				info: jest.fn(),
 609 | 				warn: jest.fn(),
 610 | 				error: jest.fn(),
 611 | 				debug: jest.fn(),
 612 | 				success: jest.fn()
 613 | 			}
 614 | 		});
 615 | 
 616 | 		// generateTaskFiles is currently commented out in parse-prd.js
 617 | 	});
 618 | 
 619 | 	test('should overwrite tasks.json when force flag is true', async () => {
 620 | 		// Setup mocks to simulate tasks.json already exists
 621 | 		fs.default.existsSync.mockImplementation((p) => {
 622 | 			if (p === 'tasks/tasks.json') return true; // Output file exists
 623 | 			if (p === 'tasks') return true; // Directory exists
 624 | 			return false;
 625 | 		});
 626 | 
 627 | 		// Call the function with force=true to allow overwrite and mcpLog to force non-streaming mode
 628 | 		await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 629 | 			force: true,
 630 | 			tag: 'master',
 631 | 			mcpLog: {
 632 | 				info: jest.fn(),
 633 | 				warn: jest.fn(),
 634 | 				error: jest.fn(),
 635 | 				debug: jest.fn(),
 636 | 				success: jest.fn()
 637 | 			}
 638 | 		});
 639 | 
 640 | 		// Verify prompt was NOT called (confirmation happens at CLI level, not in core function)
 641 | 		expect(promptYesNo).not.toHaveBeenCalled();
 642 | 
 643 | 		// Verify the file was written after force overwrite
 644 | 		expect(fs.default.writeFileSync).toHaveBeenCalledWith(
 645 | 			'tasks/tasks.json',
 646 | 			expect.stringContaining('"master"')
 647 | 		);
 648 | 	});
 649 | 
 650 | 	test('should throw error when tasks in tag exist without force flag in MCP mode', async () => {
 651 | 		// Setup mocks to simulate tasks.json already exists with tasks in the target tag
 652 | 		fs.default.existsSync.mockReturnValue(true);
 653 | 		// Mock readFileSync to return data with tasks in the 'master' tag
 654 | 		fs.default.readFileSync.mockReturnValueOnce(
 655 | 			JSON.stringify(existingTasksData)
 656 | 		);
 657 | 
 658 | 		// Call the function with mcpLog to make it think it's in MCP mode (which throws instead of process.exit)
 659 | 		await expect(
 660 | 			parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 661 | 				tag: 'master',
 662 | 				mcpLog: {
 663 | 					info: jest.fn(),
 664 | 					warn: jest.fn(),
 665 | 					error: jest.fn(),
 666 | 					debug: jest.fn(),
 667 | 					success: jest.fn()
 668 | 				}
 669 | 			})
 670 | 		).rejects.toThrow('already contains');
 671 | 
 672 | 		// Verify prompt was NOT called
 673 | 		expect(promptYesNo).not.toHaveBeenCalled();
 674 | 
 675 | 		// Verify the file was NOT written
 676 | 		expect(fs.default.writeFileSync).not.toHaveBeenCalled();
 677 | 	});
 678 | 
 679 | 	test('should throw error when tasks in tag exist without force flag in CLI mode', async () => {
 680 | 		// Setup mocks to simulate tasks.json already exists with tasks in the target tag
 681 | 		fs.default.existsSync.mockReturnValue(true);
 682 | 		fs.default.readFileSync.mockReturnValueOnce(
 683 | 			JSON.stringify(existingTasksData)
 684 | 		);
 685 | 
 686 | 		// Call the function without mcpLog (CLI mode) and expect it to throw an error
 687 | 		// In test environment, process.exit is prevented and error is thrown instead
 688 | 		await expect(
 689 | 			parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, { tag: 'master' })
 690 | 		).rejects.toThrow('process.exit was called with code 1');
 691 | 
 692 | 		// Verify the file was NOT written
 693 | 		expect(fs.default.writeFileSync).not.toHaveBeenCalled();
 694 | 	});
 695 | 
 696 | 	test('should append new tasks when append option is true', async () => {
 697 | 		// Setup mocks to simulate tasks.json already exists
 698 | 		fs.default.existsSync.mockReturnValue(true);
 699 | 
 700 | 		// Mock for reading existing tasks in tagged format
 701 | 		readJSON.mockReturnValue(existingTasksData);
 702 | 		// Mock readFileSync to return the raw content for the initial check
 703 | 		fs.default.readFileSync.mockReturnValueOnce(
 704 | 			JSON.stringify(existingTasksData)
 705 | 		);
 706 | 
 707 | 		// Mock generateObjectService to return new tasks with continuing IDs
 708 | 		generateObjectService.mockResolvedValueOnce({
 709 | 			mainResult: { object: newTasksClaudeResponse },
 710 | 			telemetryData: {}
 711 | 		});
 712 | 
 713 | 		// Call the function with append option and mcpLog to force non-streaming mode
 714 | 		const result = await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 2, {
 715 | 			tag: 'master',
 716 | 			append: true,
 717 | 			mcpLog: {
 718 | 				info: jest.fn(),
 719 | 				warn: jest.fn(),
 720 | 				error: jest.fn(),
 721 | 				debug: jest.fn(),
 722 | 				success: jest.fn()
 723 | 			}
 724 | 		});
 725 | 
 726 | 		// Verify prompt was NOT called (no confirmation needed for append)
 727 | 		expect(promptYesNo).not.toHaveBeenCalled();
 728 | 
 729 | 		// Verify the file was written with merged tasks in the correct tag
 730 | 		expect(fs.default.writeFileSync).toHaveBeenCalledWith(
 731 | 			'tasks/tasks.json',
 732 | 			expect.stringContaining('"master"')
 733 | 		);
 734 | 
 735 | 		// Verify the result contains merged tasks
 736 | 		expect(result).toEqual({
 737 | 			success: true,
 738 | 			tasksPath: 'tasks/tasks.json',
 739 | 			telemetryData: {}
 740 | 		});
 741 | 
 742 | 		// Verify that the written data contains 4 tasks (2 existing + 2 new)
 743 | 		const writtenDataString = fs.default.writeFileSync.mock.calls[0][1];
 744 | 		const writtenData = JSON.parse(writtenDataString);
 745 | 		expect(writtenData.master.tasks.length).toBe(4);
 746 | 	});
 747 | 
 748 | 	test('should skip prompt and not overwrite when append is true', async () => {
 749 | 		// Setup mocks to simulate tasks.json already exists
 750 | 		fs.default.existsSync.mockReturnValue(true);
 751 | 		fs.default.readFileSync.mockReturnValueOnce(
 752 | 			JSON.stringify(existingTasksData)
 753 | 		);
 754 | 
 755 | 		// Ensure generateObjectService returns proper tasks
 756 | 		generateObjectService.mockResolvedValue({
 757 | 			mainResult: {
 758 | 				tasks: [
 759 | 					{
 760 | 						id: 1,
 761 | 						title: 'Test Task 1',
 762 | 						priority: 'high',
 763 | 						description: 'Test description 1',
 764 | 						status: 'pending',
 765 | 						dependencies: []
 766 | 					},
 767 | 					{
 768 | 						id: 2,
 769 | 						title: 'Test Task 2',
 770 | 						priority: 'medium',
 771 | 						description: 'Test description 2',
 772 | 						status: 'pending',
 773 | 						dependencies: []
 774 | 					},
 775 | 					{
 776 | 						id: 3,
 777 | 						title: 'Test Task 3',
 778 | 						priority: 'low',
 779 | 						description: 'Test description 3',
 780 | 						status: 'pending',
 781 | 						dependencies: []
 782 | 					}
 783 | 				]
 784 | 			},
 785 | 			telemetryData: {}
 786 | 		});
 787 | 
 788 | 		// Call the function with append option
 789 | 		await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 790 | 			tag: 'master',
 791 | 			append: true
 792 | 		});
 793 | 
 794 | 		// Verify prompt was NOT called with append flag
 795 | 		expect(promptYesNo).not.toHaveBeenCalled();
 796 | 	});
 797 | 
 798 | 	describe('Streaming vs Non-Streaming Modes', () => {
 799 | 		test('should use non-streaming when reportProgress function is provided (streaming disabled)', async () => {
 800 | 			// Setup mocks to simulate normal conditions (no existing output file)
 801 | 			fs.default.existsSync.mockImplementation((path) => {
 802 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
 803 | 				if (path === 'tasks') return true; // Directory exists
 804 | 				return false;
 805 | 			});
 806 | 
 807 | 			// Mock progress reporting function
 808 | 			const mockReportProgress = jest.fn(() => Promise.resolve());
 809 | 
 810 | 			// Mock JSONParser instance
 811 | 			const mockParser = {
 812 | 				onValue: jest.fn(),
 813 | 				onError: jest.fn(),
 814 | 				write: jest.fn(),
 815 | 				end: jest.fn()
 816 | 			};
 817 | 			JSONParser.mockReturnValue(mockParser);
 818 | 
 819 | 			// Call the function with reportProgress - with streaming disabled, should use non-streaming
 820 | 			const result = await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 821 | 				reportProgress: mockReportProgress
 822 | 			});
 823 | 
 824 | 			// With streaming disabled, should use generateObjectService instead
 825 | 			expect(generateObjectService).toHaveBeenCalled();
 826 | 
 827 | 			// Verify streamObjectService was NOT called (streaming is disabled)
 828 | 			expect(streamObjectService).not.toHaveBeenCalled();
 829 | 
 830 | 			// Verify progress reporting was still called
 831 | 			expect(mockReportProgress).toHaveBeenCalled();
 832 | 
 833 | 			// Verify result structure
 834 | 			expect(result).toEqual({
 835 | 				success: true,
 836 | 				tasksPath: 'tasks/tasks.json',
 837 | 				telemetryData: {}
 838 | 			});
 839 | 		});
 840 | 
 841 | 		test.skip('should fallback to non-streaming when streaming fails with specific errors (streaming disabled)', async () => {
 842 | 			// Setup mocks to simulate normal conditions (no existing output file)
 843 | 			fs.default.existsSync.mockImplementation((path) => {
 844 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
 845 | 				if (path === 'tasks') return true; // Directory exists
 846 | 				return false;
 847 | 			});
 848 | 
 849 | 			// Mock progress reporting function
 850 | 			const mockReportProgress = jest.fn(() => Promise.resolve());
 851 | 
 852 | 			// Mock streamObjectService to return a stream that fails during processing
 853 | 			streamObjectService.mockImplementationOnce(async () => {
 854 | 				return {
 855 | 					mainResult: {
 856 | 						get partialObjectStream() {
 857 | 							return (async function* () {
 858 | 								throw new Error('Stream processing failed');
 859 | 							})();
 860 | 						},
 861 | 						usage: Promise.resolve(null),
 862 | 						object: Promise.resolve(null)
 863 | 					},
 864 | 					providerName: 'anthropic',
 865 | 					modelId: 'claude-3-5-sonnet-20241022',
 866 | 					telemetryData: {}
 867 | 				};
 868 | 			});
 869 | 
 870 | 			// Ensure generateObjectService returns tasks for fallback
 871 | 			generateObjectService.mockResolvedValue({
 872 | 				mainResult: {
 873 | 					tasks: [
 874 | 						{
 875 | 							id: 1,
 876 | 							title: 'Test Task 1',
 877 | 							priority: 'high',
 878 | 							description: 'Test description 1',
 879 | 							status: 'pending',
 880 | 							dependencies: []
 881 | 						},
 882 | 						{
 883 | 							id: 2,
 884 | 							title: 'Test Task 2',
 885 | 							priority: 'medium',
 886 | 							description: 'Test description 2',
 887 | 							status: 'pending',
 888 | 							dependencies: []
 889 | 						},
 890 | 						{
 891 | 							id: 3,
 892 | 							title: 'Test Task 3',
 893 | 							priority: 'low',
 894 | 							description: 'Test description 3',
 895 | 							status: 'pending',
 896 | 							dependencies: []
 897 | 						}
 898 | 					]
 899 | 				},
 900 | 				telemetryData: {}
 901 | 			});
 902 | 
 903 | 			// Call the function with reportProgress to trigger streaming path
 904 | 			const result = await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 905 | 				reportProgress: mockReportProgress
 906 | 			});
 907 | 
 908 | 			// Verify streamObjectService was called first (streaming attempt)
 909 | 			expect(streamObjectService).toHaveBeenCalled();
 910 | 
 911 | 			// Verify generateObjectService was called as fallback
 912 | 			expect(generateObjectService).toHaveBeenCalled();
 913 | 
 914 | 			// Verify result structure (should succeed via fallback)
 915 | 			expect(result).toEqual({
 916 | 				success: true,
 917 | 				tasksPath: 'tasks/tasks.json',
 918 | 				telemetryData: {}
 919 | 			});
 920 | 		});
 921 | 
 922 | 		test('should use non-streaming when reportProgress is not provided', async () => {
 923 | 			// Setup mocks to simulate normal conditions (no existing output file)
 924 | 			fs.default.existsSync.mockImplementation((path) => {
 925 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
 926 | 				if (path === 'tasks') return true; // Directory exists
 927 | 				return false;
 928 | 			});
 929 | 
 930 | 			// Call the function without reportProgress but with mcpLog to force non-streaming path
 931 | 			const result = await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 932 | 				mcpLog: {
 933 | 					info: jest.fn(),
 934 | 					warn: jest.fn(),
 935 | 					error: jest.fn(),
 936 | 					debug: jest.fn(),
 937 | 					success: jest.fn()
 938 | 				}
 939 | 			});
 940 | 
 941 | 			// Verify generateObjectService was called (non-streaming path)
 942 | 			expect(generateObjectService).toHaveBeenCalled();
 943 | 
 944 | 			// Verify streamObjectService was NOT called (streaming path)
 945 | 			expect(streamObjectService).not.toHaveBeenCalled();
 946 | 
 947 | 			// Verify result structure
 948 | 			expect(result).toEqual({
 949 | 				success: true,
 950 | 				tasksPath: 'tasks/tasks.json',
 951 | 				telemetryData: {}
 952 | 			});
 953 | 		});
 954 | 
 955 | 		test('should handle research flag with non-streaming (streaming disabled)', async () => {
 956 | 			// Setup mocks to simulate normal conditions
 957 | 			fs.default.existsSync.mockImplementation((path) => {
 958 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
 959 | 				if (path === 'tasks') return true; // Directory exists
 960 | 				return false;
 961 | 			});
 962 | 
 963 | 			// Mock progress reporting function
 964 | 			const mockReportProgress = jest.fn(() => Promise.resolve());
 965 | 
 966 | 			// Call with reportProgress + research - with streaming disabled, should use non-streaming
 967 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 968 | 				reportProgress: mockReportProgress,
 969 | 				research: true
 970 | 			});
 971 | 
 972 | 			// With streaming disabled, should use generateObjectService with research role
 973 | 			expect(generateObjectService).toHaveBeenCalledWith(
 974 | 				expect.objectContaining({
 975 | 					role: 'research'
 976 | 				})
 977 | 			);
 978 | 			expect(streamObjectService).not.toHaveBeenCalled();
 979 | 		});
 980 | 
 981 | 		test('should handle research flag with non-streaming', async () => {
 982 | 			// Setup mocks to simulate normal conditions
 983 | 			fs.default.existsSync.mockImplementation((path) => {
 984 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
 985 | 				if (path === 'tasks') return true; // Directory exists
 986 | 				return false;
 987 | 			});
 988 | 
 989 | 			// Call without reportProgress but with mcpLog (non-streaming) + research
 990 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
 991 | 				research: true,
 992 | 				mcpLog: {
 993 | 					info: jest.fn(),
 994 | 					warn: jest.fn(),
 995 | 					error: jest.fn(),
 996 | 					debug: jest.fn(),
 997 | 					success: jest.fn()
 998 | 				}
 999 | 			});
1000 | 
1001 | 			// Verify non-streaming path was used with research role
1002 | 			expect(generateObjectService).toHaveBeenCalledWith(
1003 | 				expect.objectContaining({
1004 | 					role: 'research'
1005 | 				})
1006 | 			);
1007 | 			expect(streamObjectService).not.toHaveBeenCalled();
1008 | 		});
1009 | 
1010 | 		test('should use non-streaming for CLI text mode (streaming disabled)', async () => {
1011 | 			// Setup mocks to simulate normal conditions
1012 | 			fs.default.existsSync.mockImplementation((path) => {
1013 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
1014 | 				if (path === 'tasks') return true; // Directory exists
1015 | 				return false;
1016 | 			});
1017 | 
1018 | 			// Call without mcpLog and without reportProgress (CLI text mode)
1019 | 			const result = await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3);
1020 | 
1021 | 			// With streaming disabled, should use generateObjectService even in CLI text mode
1022 | 			expect(generateObjectService).toHaveBeenCalled();
1023 | 			expect(streamObjectService).not.toHaveBeenCalled();
1024 | 
1025 | 			// Progress tracker components may still be called for CLI mode display
1026 | 			// but the actual parsing uses non-streaming
1027 | 
1028 | 			expect(result).toEqual({
1029 | 				success: true,
1030 | 				tasksPath: 'tasks/tasks.json',
1031 | 				telemetryData: {}
1032 | 			});
1033 | 		});
1034 | 
1035 | 		test.skip('should handle parseStream with usedFallback flag - needs rewrite for streamObject', async () => {
1036 | 			// Setup mocks to simulate normal conditions
1037 | 			fs.default.existsSync.mockImplementation((path) => {
1038 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
1039 | 				if (path === 'tasks') return true; // Directory exists
1040 | 				return false;
1041 | 			});
1042 | 
1043 | 			// Mock progress reporting function
1044 | 			const mockReportProgress = jest.fn(() => Promise.resolve());
1045 | 
1046 | 			// Mock parseStream to return usedFallback: true
1047 | 			parseStream.mockResolvedValueOnce({
1048 | 				items: [{ id: 1, title: 'Test Task', priority: 'high' }],
1049 | 				accumulatedText:
1050 | 					'{"tasks":[{"id":1,"title":"Test Task","priority":"high"}]}',
1051 | 				estimatedTokens: 50,
1052 | 				usedFallback: true // This triggers fallback reporting
1053 | 			});
1054 | 
1055 | 			// Call with streaming
1056 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
1057 | 				reportProgress: mockReportProgress
1058 | 			});
1059 | 
1060 | 			// Verify that usedFallback scenario was handled
1061 | 			expect(parseStream).toHaveBeenCalledWith(
1062 | 				expect.anything(),
1063 | 				expect.objectContaining({
1064 | 					jsonPaths: ['$.tasks.*'],
1065 | 					onProgress: expect.any(Function),
1066 | 					onError: expect.any(Function),
1067 | 					estimateTokens: expect.any(Function),
1068 | 					expectedTotal: 3,
1069 | 					fallbackItemExtractor: expect.any(Function)
1070 | 				})
1071 | 			);
1072 | 		});
1073 | 
1074 | 		test.skip('should handle StreamingError types for fallback - needs rewrite for streamObject', async () => {
1075 | 			// Setup mocks to simulate normal conditions
1076 | 			fs.default.existsSync.mockImplementation((path) => {
1077 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
1078 | 				if (path === 'tasks') return true; // Directory exists
1079 | 				return false;
1080 | 			});
1081 | 
1082 | 			// Test different StreamingError types that should trigger fallback
1083 | 			const streamingErrors = [
1084 | 				{
1085 | 					message: 'Stream object is not iterable',
1086 | 					code: STREAMING_ERROR_CODES.STREAM_NOT_ITERABLE
1087 | 				},
1088 | 				{
1089 | 					message: 'Failed to process AI text stream',
1090 | 					code: STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED
1091 | 				},
1092 | 				{
1093 | 					message: 'textStream is not async iterable',
1094 | 					code: STREAMING_ERROR_CODES.NOT_ASYNC_ITERABLE
1095 | 				}
1096 | 			];
1097 | 
1098 | 			for (const errorConfig of streamingErrors) {
1099 | 				// Clear mocks for each iteration
1100 | 				jest.clearAllMocks();
1101 | 
1102 | 				// Setup mocks again
1103 | 				fs.default.existsSync.mockImplementation((path) => {
1104 | 					if (path === 'tasks/tasks.json') return false;
1105 | 					if (path === 'tasks') return true;
1106 | 					return false;
1107 | 				});
1108 | 				fs.default.readFileSync.mockReturnValue(samplePRDContent);
1109 | 				generateObjectService.mockResolvedValue({
1110 | 					mainResult: { object: sampleClaudeResponse },
1111 | 					telemetryData: {}
1112 | 				});
1113 | 
1114 | 				// Mock streamTextService to fail with StreamingError
1115 | 				const error = new StreamingError(errorConfig.message, errorConfig.code);
1116 | 				streamTextService.mockRejectedValueOnce(error);
1117 | 
1118 | 				// Mock progress reporting function
1119 | 				const mockReportProgress = jest.fn(() => Promise.resolve());
1120 | 
1121 | 				// Call with streaming (should fallback to non-streaming)
1122 | 				const result = await parsePRD(
1123 | 					'path/to/prd.txt',
1124 | 					'tasks/tasks.json',
1125 | 					3,
1126 | 					{
1127 | 						reportProgress: mockReportProgress
1128 | 					}
1129 | 				);
1130 | 
1131 | 				// Verify streaming was attempted first
1132 | 				expect(streamTextService).toHaveBeenCalled();
1133 | 
1134 | 				// Verify fallback to non-streaming occurred
1135 | 				expect(generateObjectService).toHaveBeenCalled();
1136 | 
1137 | 				// Verify successful result despite streaming failure
1138 | 				expect(result).toEqual({
1139 | 					success: true,
1140 | 					tasksPath: 'tasks/tasks.json',
1141 | 					telemetryData: {}
1142 | 				});
1143 | 			}
1144 | 		});
1145 | 
1146 | 		test.skip('should handle progress tracker integration in CLI streaming mode - needs rewrite for streamObject', async () => {
1147 | 			// Setup mocks to simulate normal conditions
1148 | 			fs.default.existsSync.mockImplementation((path) => {
1149 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
1150 | 				if (path === 'tasks') return true; // Directory exists
1151 | 				return false;
1152 | 			});
1153 | 
1154 | 			// Mock progress tracker methods
1155 | 			const mockProgressTracker = {
1156 | 				start: jest.fn(),
1157 | 				stop: jest.fn(),
1158 | 				cleanup: jest.fn(),
1159 | 				addTaskLine: jest.fn(),
1160 | 				updateTokens: jest.fn(),
1161 | 				getSummary: jest.fn().mockReturnValue({
1162 | 					taskPriorities: { high: 1, medium: 0, low: 0 },
1163 | 					elapsedTime: 1000,
1164 | 					actionVerb: 'generated'
1165 | 				})
1166 | 			};
1167 | 			createParsePrdTracker.mockReturnValue(mockProgressTracker);
1168 | 
1169 | 			// Call in CLI text mode (no mcpLog, no reportProgress)
1170 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3);
1171 | 
1172 | 			// Verify progress tracker was created and used
1173 | 			expect(createParsePrdTracker).toHaveBeenCalledWith({
1174 | 				numUnits: 3,
1175 | 				unitName: 'task',
1176 | 				append: false
1177 | 			});
1178 | 			expect(mockProgressTracker.start).toHaveBeenCalled();
1179 | 			expect(mockProgressTracker.cleanup).toHaveBeenCalled();
1180 | 
1181 | 			// Verify UI display functions were called
1182 | 			expect(displayParsePrdStart).toHaveBeenCalled();
1183 | 			expect(displayParsePrdSummary).toHaveBeenCalled();
1184 | 		});
1185 | 
1186 | 		test.skip('should handle onProgress callback during streaming - needs rewrite for streamObject', async () => {
1187 | 			// Setup mocks to simulate normal conditions
1188 | 			fs.default.existsSync.mockImplementation((path) => {
1189 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
1190 | 				if (path === 'tasks') return true; // Directory exists
1191 | 				return false;
1192 | 			});
1193 | 
1194 | 			// Mock progress reporting function
1195 | 			const mockReportProgress = jest.fn(() => Promise.resolve());
1196 | 
1197 | 			// Mock parseStream to call onProgress
1198 | 			parseStream.mockImplementation(async (stream, options) => {
1199 | 				// Simulate calling onProgress during parsing
1200 | 				if (options.onProgress) {
1201 | 					await options.onProgress(
1202 | 						{ title: 'Test Task', priority: 'high' },
1203 | 						{ currentCount: 1, estimatedTokens: 50 }
1204 | 					);
1205 | 				}
1206 | 				return {
1207 | 					items: [{ id: 1, title: 'Test Task', priority: 'high' }],
1208 | 					accumulatedText:
1209 | 						'{"tasks":[{"id":1,"title":"Test Task","priority":"high"}]}',
1210 | 					estimatedTokens: 50,
1211 | 					usedFallback: false
1212 | 				};
1213 | 			});
1214 | 
1215 | 			// Call with streaming
1216 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
1217 | 				reportProgress: mockReportProgress
1218 | 			});
1219 | 
1220 | 			// Verify parseStream was called with correct onProgress callback
1221 | 			expect(parseStream).toHaveBeenCalledWith(
1222 | 				expect.anything(),
1223 | 				expect.objectContaining({
1224 | 					onProgress: expect.any(Function)
1225 | 				})
1226 | 			);
1227 | 
1228 | 			// Verify progress was reported during streaming
1229 | 			expect(mockReportProgress).toHaveBeenCalled();
1230 | 		});
1231 | 
1232 | 		test.skip('should not re-throw non-streaming errors during fallback - needs rewrite for streamObject', async () => {
1233 | 			// Setup mocks to simulate normal conditions
1234 | 			fs.default.existsSync.mockImplementation((path) => {
1235 | 				if (path === 'tasks/tasks.json') return false; // Output file doesn't exist
1236 | 				if (path === 'tasks') return true; // Directory exists
1237 | 				return false;
1238 | 			});
1239 | 
1240 | 			// Mock progress reporting function
1241 | 			const mockReportProgress = jest.fn(() => Promise.resolve());
1242 | 
1243 | 			// Mock streamTextService to fail with NON-streaming error
1244 | 			streamTextService.mockRejectedValueOnce(
1245 | 				new Error('AI API rate limit exceeded')
1246 | 			);
1247 | 
1248 | 			// Call with streaming - should re-throw non-streaming errors
1249 | 			await expect(
1250 | 				parsePRD('path/to/prd.txt', 'tasks/tasks.json', 3, {
1251 | 					reportProgress: mockReportProgress
1252 | 				})
1253 | 			).rejects.toThrow('AI API rate limit exceeded');
1254 | 
1255 | 			// Verify streaming was attempted
1256 | 			expect(streamTextService).toHaveBeenCalled();
1257 | 
1258 | 			// Verify fallback was NOT attempted (error was re-thrown)
1259 | 			expect(generateObjectService).not.toHaveBeenCalled();
1260 | 		});
1261 | 	});
1262 | 
1263 | 	describe('Dynamic Task Generation', () => {
1264 | 		test('should use dynamic prompting when numTasks is 0', async () => {
1265 | 			// Setup mocks to simulate normal conditions (no existing output file)
1266 | 			fs.default.existsSync.mockImplementation((p) => {
1267 | 				if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
1268 | 				if (p === 'tasks') return true; // Directory exists
1269 | 				return false;
1270 | 			});
1271 | 
1272 | 			// Call the function with numTasks=0 for dynamic generation and mcpLog to force non-streaming mode
1273 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 0, {
1274 | 				tag: 'master',
1275 | 				mcpLog: {
1276 | 					info: jest.fn(),
1277 | 					warn: jest.fn(),
1278 | 					error: jest.fn(),
1279 | 					debug: jest.fn(),
1280 | 					success: jest.fn()
1281 | 				}
1282 | 			});
1283 | 
1284 | 			// Verify generateObjectService was called
1285 | 			expect(generateObjectService).toHaveBeenCalled();
1286 | 
1287 | 			// Get the call arguments to verify the prompt
1288 | 			const callArgs = generateObjectService.mock.calls[0][0];
1289 | 			expect(callArgs.prompt).toContain('an appropriate number of');
1290 | 			expect(callArgs.prompt).not.toContain('approximately 0');
1291 | 		});
1292 | 
1293 | 		test('should use specific count prompting when numTasks is positive', async () => {
1294 | 			// Setup mocks to simulate normal conditions (no existing output file)
1295 | 			fs.default.existsSync.mockImplementation((p) => {
1296 | 				if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
1297 | 				if (p === 'tasks') return true; // Directory exists
1298 | 				return false;
1299 | 			});
1300 | 
1301 | 			// Call the function with specific numTasks and mcpLog to force non-streaming mode
1302 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 5, {
1303 | 				tag: 'master',
1304 | 				mcpLog: {
1305 | 					info: jest.fn(),
1306 | 					warn: jest.fn(),
1307 | 					error: jest.fn(),
1308 | 					debug: jest.fn(),
1309 | 					success: jest.fn()
1310 | 				}
1311 | 			});
1312 | 
1313 | 			// Verify generateObjectService was called
1314 | 			expect(generateObjectService).toHaveBeenCalled();
1315 | 
1316 | 			// Get the call arguments to verify the prompt
1317 | 			const callArgs = generateObjectService.mock.calls[0][0];
1318 | 			expect(callArgs.prompt).toContain('approximately 5');
1319 | 			expect(callArgs.prompt).not.toContain('an appropriate number of');
1320 | 		});
1321 | 
1322 | 		test('should accept 0 as valid numTasks value', async () => {
1323 | 			// Setup mocks to simulate normal conditions (no existing output file)
1324 | 			fs.default.existsSync.mockImplementation((p) => {
1325 | 				if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
1326 | 				if (p === 'tasks') return true; // Directory exists
1327 | 				return false;
1328 | 			});
1329 | 
1330 | 			// Call the function with numTasks=0 and mcpLog to force non-streaming mode - should not throw error
1331 | 			const result = await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 0, {
1332 | 				tag: 'master',
1333 | 				mcpLog: {
1334 | 					info: jest.fn(),
1335 | 					warn: jest.fn(),
1336 | 					error: jest.fn(),
1337 | 					debug: jest.fn(),
1338 | 					success: jest.fn()
1339 | 				}
1340 | 			});
1341 | 
1342 | 			// Verify it completed successfully
1343 | 			expect(result).toEqual({
1344 | 				success: true,
1345 | 				tasksPath: 'tasks/tasks.json',
1346 | 				telemetryData: {}
1347 | 			});
1348 | 		});
1349 | 
1350 | 		test('should use dynamic prompting when numTasks is negative (no validation in main module)', async () => {
1351 | 			// Setup mocks to simulate normal conditions (no existing output file)
1352 | 			fs.default.existsSync.mockImplementation((p) => {
1353 | 				if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
1354 | 				if (p === 'tasks') return true; // Directory exists
1355 | 				return false;
1356 | 			});
1357 | 
1358 | 			// Call the function with negative numTasks and mcpLog to force non-streaming mode
1359 | 			// Note: The main parse-prd.js module doesn't validate numTasks - validation happens at CLI/MCP level
1360 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', -5, {
1361 | 				tag: 'master',
1362 | 				mcpLog: {
1363 | 					info: jest.fn(),
1364 | 					warn: jest.fn(),
1365 | 					error: jest.fn(),
1366 | 					debug: jest.fn(),
1367 | 					success: jest.fn()
1368 | 				}
1369 | 			});
1370 | 
1371 | 			// Verify generateObjectService was called
1372 | 			expect(generateObjectService).toHaveBeenCalled();
1373 | 			const callArgs = generateObjectService.mock.calls[0][0];
1374 | 			// Negative values are treated as <= 0, so should use dynamic prompting
1375 | 			expect(callArgs.prompt).toContain('an appropriate number of');
1376 | 			expect(callArgs.prompt).not.toContain('approximately -5');
1377 | 		});
1378 | 	});
1379 | 
1380 | 	describe('Configuration Integration', () => {
1381 | 		test('should use dynamic prompting when numTasks is null', async () => {
1382 | 			// Setup mocks to simulate normal conditions (no existing output file)
1383 | 			fs.default.existsSync.mockImplementation((p) => {
1384 | 				if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
1385 | 				if (p === 'tasks') return true; // Directory exists
1386 | 				return false;
1387 | 			});
1388 | 
1389 | 			// Call the function with null numTasks and mcpLog to force non-streaming mode
1390 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', null, {
1391 | 				tag: 'master',
1392 | 				mcpLog: {
1393 | 					info: jest.fn(),
1394 | 					warn: jest.fn(),
1395 | 					error: jest.fn(),
1396 | 					debug: jest.fn(),
1397 | 					success: jest.fn()
1398 | 				}
1399 | 			});
1400 | 
1401 | 			// Verify generateObjectService was called with dynamic prompting
1402 | 			expect(generateObjectService).toHaveBeenCalled();
1403 | 			const callArgs = generateObjectService.mock.calls[0][0];
1404 | 			expect(callArgs.prompt).toContain('an appropriate number of');
1405 | 		});
1406 | 
1407 | 		test('should use dynamic prompting when numTasks is invalid string', async () => {
1408 | 			// Setup mocks to simulate normal conditions (no existing output file)
1409 | 			fs.default.existsSync.mockImplementation((p) => {
1410 | 				if (p === 'tasks/tasks.json') return false; // Output file doesn't exist
1411 | 				if (p === 'tasks') return true; // Directory exists
1412 | 				return false;
1413 | 			});
1414 | 
1415 | 			// Call the function with invalid numTasks (string that's not a number) and mcpLog to force non-streaming mode
1416 | 			await parsePRD('path/to/prd.txt', 'tasks/tasks.json', 'invalid', {
1417 | 				tag: 'master',
1418 | 				mcpLog: {
1419 | 					info: jest.fn(),
1420 | 					warn: jest.fn(),
1421 | 					error: jest.fn(),
1422 | 					debug: jest.fn(),
1423 | 					success: jest.fn()
1424 | 				}
1425 | 			});
1426 | 
1427 | 			// Verify generateObjectService was called with dynamic prompting
1428 | 			// Note: The main module doesn't validate - it just uses the value as-is
1429 | 			// Since 'invalid' > 0 is false, it uses dynamic prompting
1430 | 			expect(generateObjectService).toHaveBeenCalled();
1431 | 			const callArgs = generateObjectService.mock.calls[0][0];
1432 | 			expect(callArgs.prompt).toContain('an appropriate number of');
1433 | 		});
1434 | 	});
1435 | });
1436 | 
```

--------------------------------------------------------------------------------
/scripts/modules/task-manager/tag-management.js:
--------------------------------------------------------------------------------

```javascript
   1 | import path from 'path';
   2 | import fs from 'fs';
   3 | import inquirer from 'inquirer';
   4 | import chalk from 'chalk';
   5 | import boxen from 'boxen';
   6 | import Table from 'cli-table3';
   7 | 
   8 | import {
   9 | 	log,
  10 | 	readJSON,
  11 | 	writeJSON,
  12 | 	getCurrentTag,
  13 | 	resolveTag,
  14 | 	getTasksForTag,
  15 | 	setTasksForTag,
  16 | 	findProjectRoot,
  17 | 	truncate
  18 | } from '../utils.js';
  19 | import { displayBanner, getStatusWithColor } from '../ui.js';
  20 | import findNextTask from './find-next-task.js';
  21 | import {
  22 | 	tryListTagsViaRemote,
  23 | 	tryUseTagViaRemote,
  24 | 	tryAddTagViaRemote
  25 | } from '@tm/bridge';
  26 | 
  27 | /**
  28 |  * Create a new tag context
  29 |  * @param {string} tasksPath - Path to the tasks.json file
  30 |  * @param {string} tagName - Name of the new tag to create
  31 |  * @param {Object} options - Options object
  32 |  * @param {boolean} [options.copyFromCurrent=false] - Whether to copy tasks from current tag
  33 |  * @param {string} [options.copyFromTag] - Specific tag to copy tasks from
  34 |  * @param {string} [options.description] - Optional description for the tag
  35 |  * @param {Object} context - Context object containing session and projectRoot
  36 |  * @param {string} [context.projectRoot] - Project root path
  37 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
  38 |  * @param {string} outputFormat - Output format (text or json)
  39 |  * @returns {Promise<Object>} Result object with tag creation details
  40 |  */
  41 | async function createTag(
  42 | 	tasksPath,
  43 | 	tagName,
  44 | 	options = {},
  45 | 	context = {},
  46 | 	outputFormat = 'text'
  47 | ) {
  48 | 	const { mcpLog, projectRoot } = context;
  49 | 	const { copyFromCurrent = false, copyFromTag, description } = options;
  50 | 
  51 | 	// Create a consistent logFn object regardless of context
  52 | 	const logFn = mcpLog || {
  53 | 		info: (...args) => log('info', ...args),
  54 | 		warn: (...args) => log('warn', ...args),
  55 | 		error: (...args) => log('error', ...args),
  56 | 		debug: (...args) => log('debug', ...args),
  57 | 		success: (...args) => log('success', ...args)
  58 | 	};
  59 | 
  60 | 	// Check if API storage should handle this via remote
  61 | 	const remoteResult = await tryAddTagViaRemote({
  62 | 		tagName,
  63 | 		projectRoot: projectRoot || findProjectRoot(),
  64 | 		isMCP: !!mcpLog,
  65 | 		outputFormat,
  66 | 		report: (level, ...args) => logFn[level](...args)
  67 | 	});
  68 | 
  69 | 	// If remote handled it, return the result
  70 | 	if (remoteResult) {
  71 | 		if (!remoteResult.success) {
  72 | 			throw new Error(remoteResult.message || 'Remote tag creation failed');
  73 | 		}
  74 | 		if (outputFormat === 'json') {
  75 | 			return remoteResult;
  76 | 		}
  77 | 		// For text output, the bridge already displayed the message
  78 | 		return remoteResult;
  79 | 	}
  80 | 
  81 | 	// Otherwise, continue with file-based logic below
  82 | 	try {
  83 | 		// Validate tag name
  84 | 		if (!tagName || typeof tagName !== 'string') {
  85 | 			throw new Error('Tag name is required and must be a string');
  86 | 		}
  87 | 
  88 | 		// Validate tag name format (alphanumeric, hyphens, underscores only)
  89 | 		if (!/^[a-zA-Z0-9_-]+$/.test(tagName)) {
  90 | 			throw new Error(
  91 | 				'Tag name can only contain letters, numbers, hyphens, and underscores'
  92 | 			);
  93 | 		}
  94 | 
  95 | 		// Reserved tag names
  96 | 		const reservedNames = ['master', 'main', 'default'];
  97 | 		if (reservedNames.includes(tagName.toLowerCase())) {
  98 | 			throw new Error(`"${tagName}" is a reserved tag name`);
  99 | 		}
 100 | 
 101 | 		logFn.info(`Creating new tag: ${tagName}`);
 102 | 
 103 | 		// Read current tasks data
 104 | 		const data = readJSON(tasksPath, projectRoot);
 105 | 		if (!data) {
 106 | 			throw new Error(`Could not read tasks file at ${tasksPath}`);
 107 | 		}
 108 | 
 109 | 		// Use raw tagged data for tag operations - ensure we get the actual tagged structure
 110 | 		let rawData;
 111 | 		if (data._rawTaggedData) {
 112 | 			// If we have _rawTaggedData, use it (this is the clean tagged structure)
 113 | 			rawData = data._rawTaggedData;
 114 | 		} else if (data.tasks && !data.master) {
 115 | 			// This is legacy format - create a master tag structure
 116 | 			rawData = {
 117 | 				master: {
 118 | 					tasks: data.tasks,
 119 | 					metadata: data.metadata || {
 120 | 						created: new Date().toISOString(),
 121 | 						updated: new Date().toISOString(),
 122 | 						description: 'Tasks live here by default'
 123 | 					}
 124 | 				}
 125 | 			};
 126 | 		} else {
 127 | 			// This is already in tagged format, use it directly but exclude internal fields
 128 | 			rawData = {};
 129 | 			for (const [key, value] of Object.entries(data)) {
 130 | 				if (key !== '_rawTaggedData' && key !== 'tag') {
 131 | 					rawData[key] = value;
 132 | 				}
 133 | 			}
 134 | 		}
 135 | 
 136 | 		// Check if tag already exists
 137 | 		if (rawData[tagName]) {
 138 | 			throw new Error(`Tag "${tagName}" already exists`);
 139 | 		}
 140 | 
 141 | 		// Determine source for copying tasks (only if explicitly requested)
 142 | 		let sourceTasks = [];
 143 | 		if (copyFromCurrent || copyFromTag) {
 144 | 			const sourceTag = copyFromTag || getCurrentTag(projectRoot);
 145 | 			sourceTasks = getTasksForTag(rawData, sourceTag);
 146 | 
 147 | 			if (copyFromTag && sourceTasks.length === 0) {
 148 | 				logFn.warn(`Source tag "${copyFromTag}" not found or has no tasks`);
 149 | 			}
 150 | 
 151 | 			logFn.info(`Copying ${sourceTasks.length} tasks from tag "${sourceTag}"`);
 152 | 		} else {
 153 | 			logFn.info('Creating empty tag (no tasks copied)');
 154 | 		}
 155 | 
 156 | 		// Create the new tag structure in raw data
 157 | 		rawData[tagName] = {
 158 | 			tasks: [...sourceTasks], // Create a copy of the tasks array
 159 | 			metadata: {
 160 | 				created: new Date().toISOString(),
 161 | 				updated: new Date().toISOString(),
 162 | 				description:
 163 | 					description || `Tag created on ${new Date().toLocaleDateString()}`
 164 | 			}
 165 | 		};
 166 | 
 167 | 		// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
 168 | 		const cleanData = {};
 169 | 		for (const [key, value] of Object.entries(rawData)) {
 170 | 			if (key !== '_rawTaggedData') {
 171 | 				cleanData[key] = value;
 172 | 			}
 173 | 		}
 174 | 
 175 | 		// Write the clean data back to file with proper context to avoid tag corruption
 176 | 		writeJSON(tasksPath, cleanData, projectRoot);
 177 | 
 178 | 		logFn.success(`Successfully created tag "${tagName}"`);
 179 | 
 180 | 		// For JSON output, return structured data
 181 | 		if (outputFormat === 'json') {
 182 | 			return {
 183 | 				tagName,
 184 | 				created: true,
 185 | 				tasksCopied: sourceTasks.length,
 186 | 				sourceTag:
 187 | 					copyFromCurrent || copyFromTag
 188 | 						? copyFromTag || getCurrentTag(projectRoot)
 189 | 						: null,
 190 | 				description:
 191 | 					description || `Tag created on ${new Date().toLocaleDateString()}`
 192 | 			};
 193 | 		}
 194 | 
 195 | 		// For text output, display success message
 196 | 		if (outputFormat === 'text') {
 197 | 			console.log(
 198 | 				boxen(
 199 | 					chalk.green.bold('✓ Tag Created Successfully') +
 200 | 						`\n\nTag Name: ${chalk.cyan(tagName)}` +
 201 | 						`\nTasks Copied: ${chalk.yellow(sourceTasks.length)}` +
 202 | 						(copyFromCurrent || copyFromTag
 203 | 							? `\nSource Tag: ${chalk.cyan(copyFromTag || getCurrentTag(projectRoot))}`
 204 | 							: '') +
 205 | 						(description ? `\nDescription: ${chalk.gray(description)}` : ''),
 206 | 					{
 207 | 						padding: 1,
 208 | 						borderColor: 'green',
 209 | 						borderStyle: 'round',
 210 | 						margin: { top: 1, bottom: 1 }
 211 | 					}
 212 | 				)
 213 | 			);
 214 | 		}
 215 | 
 216 | 		return {
 217 | 			tagName,
 218 | 			created: true,
 219 | 			tasksCopied: sourceTasks.length,
 220 | 			sourceTag:
 221 | 				copyFromCurrent || copyFromTag
 222 | 					? copyFromTag || getCurrentTag(projectRoot)
 223 | 					: null,
 224 | 			description:
 225 | 				description || `Tag created on ${new Date().toLocaleDateString()}`
 226 | 		};
 227 | 	} catch (error) {
 228 | 		logFn.error(`Error creating tag: ${error.message}`);
 229 | 		throw error;
 230 | 	}
 231 | }
 232 | 
 233 | /**
 234 |  * Delete an existing tag
 235 |  * @param {string} tasksPath - Path to the tasks.json file
 236 |  * @param {string} tagName - Name of the tag to delete
 237 |  * @param {Object} options - Options object
 238 |  * @param {boolean} [options.yes=false] - Skip confirmation prompts
 239 |  * @param {Object} context - Context object containing session and projectRoot
 240 |  * @param {string} [context.projectRoot] - Project root path
 241 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
 242 |  * @param {string} outputFormat - Output format (text or json)
 243 |  * @returns {Promise<Object>} Result object with deletion details
 244 |  */
 245 | async function deleteTag(
 246 | 	tasksPath,
 247 | 	tagName,
 248 | 	options = {},
 249 | 	context = {},
 250 | 	outputFormat = 'text'
 251 | ) {
 252 | 	const { mcpLog, projectRoot } = context;
 253 | 	const { yes = false } = options;
 254 | 
 255 | 	// Create a consistent logFn object regardless of context
 256 | 	const logFn = mcpLog || {
 257 | 		info: (...args) => log('info', ...args),
 258 | 		warn: (...args) => log('warn', ...args),
 259 | 		error: (...args) => log('error', ...args),
 260 | 		debug: (...args) => log('debug', ...args),
 261 | 		success: (...args) => log('success', ...args)
 262 | 	};
 263 | 
 264 | 	try {
 265 | 		// Validate tag name
 266 | 		if (!tagName || typeof tagName !== 'string') {
 267 | 			throw new Error('Tag name is required and must be a string');
 268 | 		}
 269 | 
 270 | 		// Prevent deletion of master tag
 271 | 		if (tagName === 'master') {
 272 | 			throw new Error('Cannot delete the "master" tag');
 273 | 		}
 274 | 
 275 | 		logFn.info(`Deleting tag: ${tagName}`);
 276 | 
 277 | 		// Read current tasks data
 278 | 		const data = readJSON(tasksPath, projectRoot);
 279 | 		if (!data) {
 280 | 			throw new Error(`Could not read tasks file at ${tasksPath}`);
 281 | 		}
 282 | 
 283 | 		// Use raw tagged data for tag operations - ensure we get the actual tagged structure
 284 | 		let rawData;
 285 | 		if (data._rawTaggedData) {
 286 | 			// If we have _rawTaggedData, use it (this is the clean tagged structure)
 287 | 			rawData = data._rawTaggedData;
 288 | 		} else if (data.tasks && !data.master) {
 289 | 			// This is legacy format - create a master tag structure
 290 | 			rawData = {
 291 | 				master: {
 292 | 					tasks: data.tasks,
 293 | 					metadata: data.metadata || {
 294 | 						created: new Date().toISOString(),
 295 | 						updated: new Date().toISOString(),
 296 | 						description: 'Tasks live here by default'
 297 | 					}
 298 | 				}
 299 | 			};
 300 | 		} else {
 301 | 			// This is already in tagged format, use it directly but exclude internal fields
 302 | 			rawData = {};
 303 | 			for (const [key, value] of Object.entries(data)) {
 304 | 				if (key !== '_rawTaggedData' && key !== 'tag') {
 305 | 					rawData[key] = value;
 306 | 				}
 307 | 			}
 308 | 		}
 309 | 
 310 | 		// Check if tag exists
 311 | 		if (!rawData[tagName]) {
 312 | 			throw new Error(`Tag "${tagName}" does not exist`);
 313 | 		}
 314 | 
 315 | 		// Get current tag to check if we're deleting the active tag
 316 | 		const currentTag = getCurrentTag(projectRoot);
 317 | 		const isCurrentTag = currentTag === tagName;
 318 | 
 319 | 		// Get task count for confirmation
 320 | 		const tasks = getTasksForTag(rawData, tagName);
 321 | 		const taskCount = tasks.length;
 322 | 
 323 | 		// If not forced and has tasks, require confirmation (for CLI)
 324 | 		if (!yes && taskCount > 0 && outputFormat === 'text') {
 325 | 			console.log(
 326 | 				boxen(
 327 | 					chalk.yellow.bold('⚠ WARNING: Tag Deletion') +
 328 | 						`\n\nYou are about to delete tag "${chalk.cyan(tagName)}"` +
 329 | 						`\nThis will permanently delete ${chalk.red.bold(taskCount)} tasks` +
 330 | 						'\n\nThis action cannot be undone!',
 331 | 					{
 332 | 						padding: 1,
 333 | 						borderColor: 'yellow',
 334 | 						borderStyle: 'round',
 335 | 						margin: { top: 1, bottom: 1 }
 336 | 					}
 337 | 				)
 338 | 			);
 339 | 
 340 | 			// First confirmation
 341 | 			const firstConfirm = await inquirer.prompt([
 342 | 				{
 343 | 					type: 'confirm',
 344 | 					name: 'proceed',
 345 | 					message: `Are you sure you want to delete tag "${tagName}" and its ${taskCount} tasks?`,
 346 | 					default: false
 347 | 				}
 348 | 			]);
 349 | 
 350 | 			if (!firstConfirm.proceed) {
 351 | 				logFn.info('Tag deletion cancelled by user');
 352 | 				throw new Error('Tag deletion cancelled');
 353 | 			}
 354 | 
 355 | 			// Second confirmation (double-check)
 356 | 			const secondConfirm = await inquirer.prompt([
 357 | 				{
 358 | 					type: 'input',
 359 | 					name: 'tagNameConfirm',
 360 | 					message: `To confirm deletion, please type the tag name "${tagName}":`,
 361 | 					validate: (input) => {
 362 | 						if (input === tagName) {
 363 | 							return true;
 364 | 						}
 365 | 						return `Please type exactly "${tagName}" to confirm deletion`;
 366 | 					}
 367 | 				}
 368 | 			]);
 369 | 
 370 | 			if (secondConfirm.tagNameConfirm !== tagName) {
 371 | 				logFn.info('Tag deletion cancelled - incorrect tag name confirmation');
 372 | 				throw new Error('Tag deletion cancelled');
 373 | 			}
 374 | 
 375 | 			logFn.info('Double confirmation received, proceeding with deletion...');
 376 | 		}
 377 | 
 378 | 		// Delete the tag
 379 | 		delete rawData[tagName];
 380 | 
 381 | 		// If we're deleting the current tag, switch to master
 382 | 		if (isCurrentTag) {
 383 | 			await switchCurrentTag(projectRoot, 'master');
 384 | 			logFn.info('Switched current tag to "master"');
 385 | 		}
 386 | 
 387 | 		// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
 388 | 		const cleanData = {};
 389 | 		for (const [key, value] of Object.entries(rawData)) {
 390 | 			if (key !== '_rawTaggedData') {
 391 | 				cleanData[key] = value;
 392 | 			}
 393 | 		}
 394 | 
 395 | 		// Write the clean data back to file with proper context to avoid tag corruption
 396 | 		writeJSON(tasksPath, cleanData, projectRoot);
 397 | 
 398 | 		logFn.success(`Successfully deleted tag "${tagName}"`);
 399 | 
 400 | 		// For JSON output, return structured data
 401 | 		if (outputFormat === 'json') {
 402 | 			return {
 403 | 				tagName,
 404 | 				deleted: true,
 405 | 				tasksDeleted: taskCount,
 406 | 				wasCurrentTag: isCurrentTag,
 407 | 				switchedToMaster: isCurrentTag
 408 | 			};
 409 | 		}
 410 | 
 411 | 		// For text output, display success message
 412 | 		if (outputFormat === 'text') {
 413 | 			console.log(
 414 | 				boxen(
 415 | 					chalk.red.bold('✓ Tag Deleted Successfully') +
 416 | 						`\n\nTag Name: ${chalk.cyan(tagName)}` +
 417 | 						`\nTasks Deleted: ${chalk.yellow(taskCount)}` +
 418 | 						(isCurrentTag
 419 | 							? `\n${chalk.yellow('⚠ Switched current tag to "master"')}`
 420 | 							: ''),
 421 | 					{
 422 | 						padding: 1,
 423 | 						borderColor: 'red',
 424 | 						borderStyle: 'round',
 425 | 						margin: { top: 1, bottom: 1 }
 426 | 					}
 427 | 				)
 428 | 			);
 429 | 		}
 430 | 
 431 | 		return {
 432 | 			tagName,
 433 | 			deleted: true,
 434 | 			tasksDeleted: taskCount,
 435 | 			wasCurrentTag: isCurrentTag,
 436 | 			switchedToMaster: isCurrentTag
 437 | 		};
 438 | 	} catch (error) {
 439 | 		logFn.error(`Error deleting tag: ${error.message}`);
 440 | 		throw error;
 441 | 	}
 442 | }
 443 | 
 444 | /**
 445 |  * Enhance existing tags with metadata if they don't have it
 446 |  * @param {string} tasksPath - Path to the tasks.json file
 447 |  * @param {Object} rawData - The raw tagged data
 448 |  * @param {Object} context - Context object
 449 |  * @returns {Promise<boolean>} True if any tags were enhanced
 450 |  */
 451 | async function enhanceTagsWithMetadata(tasksPath, rawData, context = {}) {
 452 | 	let enhanced = false;
 453 | 
 454 | 	try {
 455 | 		// Get file stats for creation date fallback
 456 | 		let fileCreatedDate;
 457 | 		try {
 458 | 			const stats = fs.statSync(tasksPath);
 459 | 			fileCreatedDate =
 460 | 				stats.birthtime < stats.mtime ? stats.birthtime : stats.mtime;
 461 | 		} catch (error) {
 462 | 			fileCreatedDate = new Date();
 463 | 		}
 464 | 
 465 | 		for (const [tagName, tagData] of Object.entries(rawData)) {
 466 | 			// Skip non-tag properties
 467 | 			if (
 468 | 				tagName === 'tasks' ||
 469 | 				tagName === 'tag' ||
 470 | 				tagName === '_rawTaggedData' ||
 471 | 				!tagData ||
 472 | 				typeof tagData !== 'object' ||
 473 | 				!Array.isArray(tagData.tasks)
 474 | 			) {
 475 | 				continue;
 476 | 			}
 477 | 
 478 | 			// Check if tag needs metadata enhancement
 479 | 			if (!tagData.metadata) {
 480 | 				tagData.metadata = {};
 481 | 				enhanced = true;
 482 | 			}
 483 | 
 484 | 			// Add missing metadata fields
 485 | 			if (!tagData.metadata.created) {
 486 | 				tagData.metadata.created = fileCreatedDate.toISOString();
 487 | 				enhanced = true;
 488 | 			}
 489 | 
 490 | 			if (!tagData.metadata.description) {
 491 | 				if (tagName === 'master') {
 492 | 					tagData.metadata.description = 'Tasks live here by default';
 493 | 				} else {
 494 | 					tagData.metadata.description = `Tag created on ${new Date(tagData.metadata.created).toLocaleDateString()}`;
 495 | 				}
 496 | 				enhanced = true;
 497 | 			}
 498 | 
 499 | 			// Add updated field if missing (set to created date initially)
 500 | 			if (!tagData.metadata.updated) {
 501 | 				tagData.metadata.updated = tagData.metadata.created;
 502 | 				enhanced = true;
 503 | 			}
 504 | 		}
 505 | 
 506 | 		// If we enhanced any tags, write the data back
 507 | 		if (enhanced) {
 508 | 			// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
 509 | 			const cleanData = {};
 510 | 			for (const [key, value] of Object.entries(rawData)) {
 511 | 				if (key !== '_rawTaggedData') {
 512 | 					cleanData[key] = value;
 513 | 				}
 514 | 			}
 515 | 			writeJSON(tasksPath, cleanData, context.projectRoot);
 516 | 		}
 517 | 	} catch (error) {
 518 | 		// Don't throw - just log and continue
 519 | 		const logFn = context.mcpLog || {
 520 | 			warn: (...args) => log('warn', ...args)
 521 | 		};
 522 | 		logFn.warn(`Could not enhance tag metadata: ${error.message}`);
 523 | 	}
 524 | 
 525 | 	return enhanced;
 526 | }
 527 | 
 528 | /**
 529 |  * List all available tags with metadata
 530 |  * @param {string} tasksPath - Path to the tasks.json file
 531 |  * @param {Object} options - Options object
 532 |  * @param {boolean} [options.showTaskCounts=true] - Whether to show task counts
 533 |  * @param {boolean} [options.showMetadata=false] - Whether to show metadata
 534 |  * @param {Object} context - Context object containing session and projectRoot
 535 |  * @param {string} [context.projectRoot] - Project root path
 536 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
 537 |  * @param {string} outputFormat - Output format (text or json)
 538 |  * @returns {Promise<Object>} Result object with tags list
 539 |  */
 540 | async function tags(
 541 | 	tasksPath,
 542 | 	options = {},
 543 | 	context = {},
 544 | 	outputFormat = 'text'
 545 | ) {
 546 | 	const { mcpLog, projectRoot } = context;
 547 | 	const { showTaskCounts = true, showMetadata = false } = options;
 548 | 
 549 | 	// Create a consistent logFn object regardless of context
 550 | 	const logFn = mcpLog || {
 551 | 		info: (...args) => log('info', ...args),
 552 | 		warn: (...args) => log('warn', ...args),
 553 | 		error: (...args) => log('error', ...args),
 554 | 		debug: (...args) => log('debug', ...args),
 555 | 		success: (...args) => log('success', ...args)
 556 | 	};
 557 | 
 558 | 	try {
 559 | 		logFn.info('Listing available tags');
 560 | 
 561 | 		// Try API storage first via bridge
 562 | 		const bridgeResult = await tryListTagsViaRemote({
 563 | 			projectRoot,
 564 | 			showMetadata,
 565 | 			isMCP: !!mcpLog,
 566 | 			outputFormat,
 567 | 			report: (level, ...args) => {
 568 | 				if (logFn[level]) {
 569 | 					logFn[level](...args);
 570 | 				} else {
 571 | 					logFn.info(...args);
 572 | 				}
 573 | 			}
 574 | 		});
 575 | 
 576 | 		// If bridge handled it (API storage), return the result
 577 | 		if (bridgeResult) {
 578 | 			logFn.success(`Found ${bridgeResult.totalTags} tags via API storage`);
 579 | 			return {
 580 | 				tags: bridgeResult.tags,
 581 | 				currentTag: bridgeResult.currentTag,
 582 | 				totalTags: bridgeResult.totalTags
 583 | 			};
 584 | 		}
 585 | 
 586 | 		// Fall through to file storage logic
 587 | 		logFn.info('Using file storage for tags');
 588 | 
 589 | 		// Read current tasks data
 590 | 		const data = readJSON(tasksPath, projectRoot);
 591 | 		if (!data) {
 592 | 			throw new Error(`Could not read tasks file at ${tasksPath}`);
 593 | 		}
 594 | 
 595 | 		// Get current tag
 596 | 		const currentTag = getCurrentTag(projectRoot);
 597 | 
 598 | 		// Use raw tagged data if available, otherwise use the data directly
 599 | 		const rawData = data._rawTaggedData || data;
 600 | 
 601 | 		// Enhance existing tags with metadata if they don't have it
 602 | 		await enhanceTagsWithMetadata(tasksPath, rawData, context);
 603 | 
 604 | 		// Extract all tags
 605 | 		const tagList = [];
 606 | 		for (const [tagName, tagData] of Object.entries(rawData)) {
 607 | 			// Skip non-tag properties (like legacy 'tasks' array, 'tag', '_rawTaggedData')
 608 | 			if (
 609 | 				tagName === 'tasks' ||
 610 | 				tagName === 'tag' ||
 611 | 				tagName === '_rawTaggedData' ||
 612 | 				!tagData ||
 613 | 				typeof tagData !== 'object' ||
 614 | 				!Array.isArray(tagData.tasks)
 615 | 			) {
 616 | 				continue;
 617 | 			}
 618 | 
 619 | 			const tasks = tagData.tasks || [];
 620 | 			const metadata = tagData.metadata || {};
 621 | 
 622 | 			tagList.push({
 623 | 				name: tagName,
 624 | 				isCurrent: tagName === currentTag,
 625 | 				completedTasks: tasks.filter(
 626 | 					(t) => t.status === 'done' || t.status === 'completed'
 627 | 				).length,
 628 | 				tasks: tasks || [],
 629 | 				created: metadata.created || 'Unknown',
 630 | 				description: metadata.description || 'No description'
 631 | 			});
 632 | 		}
 633 | 
 634 | 		// Sort tags: current tag first, then alphabetically
 635 | 		tagList.sort((a, b) => {
 636 | 			if (a.isCurrent) return -1;
 637 | 			if (b.isCurrent) return 1;
 638 | 			return a.name.localeCompare(b.name);
 639 | 		});
 640 | 
 641 | 		logFn.success(`Found ${tagList.length} tags`);
 642 | 
 643 | 		// For JSON output, return structured data
 644 | 		if (outputFormat === 'json') {
 645 | 			return {
 646 | 				tags: tagList,
 647 | 				currentTag,
 648 | 				totalTags: tagList.length
 649 | 			};
 650 | 		}
 651 | 
 652 | 		// For text output, display formatted table
 653 | 		if (outputFormat === 'text') {
 654 | 			if (tagList.length === 0) {
 655 | 				console.log(
 656 | 					boxen(chalk.yellow('No tags found'), {
 657 | 						padding: 1,
 658 | 						borderColor: 'yellow',
 659 | 						borderStyle: 'round',
 660 | 						margin: { top: 1, bottom: 1 }
 661 | 					})
 662 | 				);
 663 | 				return { tags: [], currentTag, totalTags: 0 };
 664 | 			}
 665 | 
 666 | 			// Create table headers based on options
 667 | 			const headers = [chalk.cyan.bold('Tag Name')];
 668 | 			if (showTaskCounts) {
 669 | 				headers.push(chalk.cyan.bold('Tasks'));
 670 | 				headers.push(chalk.cyan.bold('Completed'));
 671 | 			}
 672 | 			if (showMetadata) {
 673 | 				headers.push(chalk.cyan.bold('Created'));
 674 | 				headers.push(chalk.cyan.bold('Description'));
 675 | 			}
 676 | 
 677 | 			// Calculate dynamic column widths based on terminal width
 678 | 			const terminalWidth = Math.max(process.stdout.columns || 120, 80);
 679 | 			const usableWidth = Math.floor(terminalWidth * 0.95);
 680 | 
 681 | 			let colWidths;
 682 | 			if (showMetadata) {
 683 | 				// With metadata: Tag Name, Tasks, Completed, Created, Description
 684 | 				const widths = [0.25, 0.1, 0.12, 0.15, 0.38];
 685 | 				colWidths = widths.map((w, i) =>
 686 | 					Math.max(Math.floor(usableWidth * w), i === 0 ? 15 : 8)
 687 | 				);
 688 | 			} else {
 689 | 				// Without metadata: Tag Name, Tasks, Completed
 690 | 				const widths = [0.7, 0.15, 0.15];
 691 | 				colWidths = widths.map((w, i) =>
 692 | 					Math.max(Math.floor(usableWidth * w), i === 0 ? 20 : 10)
 693 | 				);
 694 | 			}
 695 | 
 696 | 			const table = new Table({
 697 | 				head: headers,
 698 | 				colWidths: colWidths,
 699 | 				wordWrap: true
 700 | 			});
 701 | 
 702 | 			// Add rows
 703 | 			tagList.forEach((tag) => {
 704 | 				const row = [];
 705 | 
 706 | 				// Tag name with current indicator
 707 | 				const tagDisplay = tag.isCurrent
 708 | 					? `${chalk.green('●')} ${chalk.green.bold(tag.name)} ${chalk.gray('(current)')}`
 709 | 					: `  ${tag.name}`;
 710 | 				row.push(tagDisplay);
 711 | 
 712 | 				if (showTaskCounts) {
 713 | 					row.push(chalk.white(tag.tasks.length.toString()));
 714 | 					row.push(chalk.green(tag.completedTasks.toString()));
 715 | 				}
 716 | 
 717 | 				if (showMetadata) {
 718 | 					const createdDate =
 719 | 						tag.created !== 'Unknown'
 720 | 							? new Date(tag.created).toLocaleDateString()
 721 | 							: 'Unknown';
 722 | 					row.push(chalk.gray(createdDate));
 723 | 					row.push(chalk.gray(truncate(tag.description, 50)));
 724 | 				}
 725 | 
 726 | 				table.push(row);
 727 | 			});
 728 | 
 729 | 			// console.log(
 730 | 			// 	boxen(
 731 | 			// 		chalk.white.bold('Available Tags') +
 732 | 			// 			`\n\nCurrent Tag: ${chalk.green.bold(currentTag)}`,
 733 | 			// 		{
 734 | 			// 			padding: { top: 0, bottom: 1, left: 1, right: 1 },
 735 | 			// 			borderColor: 'blue',
 736 | 			// 			borderStyle: 'round',
 737 | 			// 			margin: { top: 1, bottom: 0 }
 738 | 			// 		}
 739 | 			// 	)
 740 | 			// );
 741 | 
 742 | 			console.log(table.toString());
 743 | 		}
 744 | 
 745 | 		return {
 746 | 			tags: tagList,
 747 | 			currentTag,
 748 | 			totalTags: tagList.length
 749 | 		};
 750 | 	} catch (error) {
 751 | 		logFn.error(`Error listing tags: ${error.message}`);
 752 | 		throw error;
 753 | 	}
 754 | }
 755 | 
 756 | /**
 757 |  * Switch to a different tag context
 758 |  * @param {string} tasksPath - Path to the tasks.json file
 759 |  * @param {string} tagName - Name of the tag to switch to
 760 |  * @param {Object} options - Options object
 761 |  * @param {Object} context - Context object containing session and projectRoot
 762 |  * @param {string} [context.projectRoot] - Project root path
 763 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
 764 |  * @param {string} outputFormat - Output format (text or json)
 765 |  * @returns {Promise<Object>} Result object with switch details
 766 |  */
 767 | async function useTag(
 768 | 	tasksPath,
 769 | 	tagName,
 770 | 	options = {},
 771 | 	context = {},
 772 | 	outputFormat = 'text'
 773 | ) {
 774 | 	const { mcpLog, projectRoot } = context;
 775 | 
 776 | 	// Create a consistent logFn object regardless of context
 777 | 	const logFn = mcpLog || {
 778 | 		info: (...args) => log('info', ...args),
 779 | 		warn: (...args) => log('warn', ...args),
 780 | 		error: (...args) => log('error', ...args),
 781 | 		debug: (...args) => log('debug', ...args),
 782 | 		success: (...args) => log('success', ...args)
 783 | 	};
 784 | 
 785 | 	try {
 786 | 		// Validate tag name
 787 | 		if (!tagName || typeof tagName !== 'string') {
 788 | 			throw new Error('Tag name is required and must be a string');
 789 | 		}
 790 | 
 791 | 		logFn.info(`Switching to tag: ${tagName}`);
 792 | 
 793 | 		// Try API storage first via bridge
 794 | 		const bridgeResult = await tryUseTagViaRemote({
 795 | 			tagName,
 796 | 			projectRoot,
 797 | 			isMCP: !!mcpLog,
 798 | 			outputFormat,
 799 | 			report: (level, ...args) => {
 800 | 				if (logFn[level]) {
 801 | 					logFn[level](...args);
 802 | 				} else {
 803 | 					logFn.info(...args);
 804 | 				}
 805 | 			}
 806 | 		});
 807 | 
 808 | 		// If bridge handled it (API storage), return the result
 809 | 		if (bridgeResult) {
 810 | 			logFn.success(
 811 | 				`Successfully switched to tag "${tagName}" via API storage`
 812 | 			);
 813 | 			return bridgeResult;
 814 | 		}
 815 | 
 816 | 		// Fall through to file storage logic
 817 | 		logFn.info('Using file storage for tag switch');
 818 | 
 819 | 		// Read current tasks data to verify tag exists
 820 | 		const data = readJSON(tasksPath, projectRoot);
 821 | 		if (!data) {
 822 | 			throw new Error(`Could not read tasks file at ${tasksPath}`);
 823 | 		}
 824 | 
 825 | 		// Use raw tagged data to check if tag exists
 826 | 		const rawData = data._rawTaggedData || data;
 827 | 
 828 | 		// Check if tag exists
 829 | 		if (!rawData[tagName]) {
 830 | 			throw new Error(`Tag "${tagName}" does not exist`);
 831 | 		}
 832 | 
 833 | 		// Get current tag
 834 | 		const previousTag = getCurrentTag(projectRoot);
 835 | 
 836 | 		// Switch to the new tag
 837 | 		await switchCurrentTag(projectRoot, tagName);
 838 | 
 839 | 		// Get task count for the new tag - read tasks specifically for this tag
 840 | 		const tagData = readJSON(tasksPath, projectRoot, tagName);
 841 | 		const tasks = tagData ? tagData.tasks || [] : [];
 842 | 		const taskCount = tasks.length;
 843 | 
 844 | 		// Find the next task to work on in this tag
 845 | 		const nextTask = findNextTask(tasks);
 846 | 
 847 | 		logFn.success(`Successfully switched to tag "${tagName}"`);
 848 | 
 849 | 		// For JSON output, return structured data
 850 | 		if (outputFormat === 'json') {
 851 | 			return {
 852 | 				previousTag,
 853 | 				currentTag: tagName,
 854 | 				switched: true,
 855 | 				taskCount,
 856 | 				nextTask
 857 | 			};
 858 | 		}
 859 | 
 860 | 		// For text output, display success message
 861 | 		if (outputFormat === 'text') {
 862 | 			let nextTaskInfo = '';
 863 | 			if (nextTask) {
 864 | 				nextTaskInfo = `\nNext Task: ${chalk.cyan(`#${nextTask.id}`)} - ${chalk.white(nextTask.title)}`;
 865 | 			} else {
 866 | 				nextTaskInfo = `\nNext Task: ${chalk.gray('No eligible tasks available')}`;
 867 | 			}
 868 | 
 869 | 			console.log(
 870 | 				boxen(
 871 | 					chalk.green.bold('✓ Tag Switched Successfully') +
 872 | 						`\n\nPrevious Tag: ${chalk.cyan(previousTag)}` +
 873 | 						`\nCurrent Tag: ${chalk.green.bold(tagName)}` +
 874 | 						`\nAvailable Tasks: ${chalk.yellow(taskCount)}` +
 875 | 						nextTaskInfo,
 876 | 					{
 877 | 						padding: 1,
 878 | 						borderColor: 'green',
 879 | 						borderStyle: 'round',
 880 | 						margin: { top: 1, bottom: 1 }
 881 | 					}
 882 | 				)
 883 | 			);
 884 | 		}
 885 | 
 886 | 		return {
 887 | 			previousTag,
 888 | 			currentTag: tagName,
 889 | 			switched: true,
 890 | 			taskCount,
 891 | 			nextTask
 892 | 		};
 893 | 	} catch (error) {
 894 | 		logFn.error(`Error switching tag: ${error.message}`);
 895 | 		throw error;
 896 | 	}
 897 | }
 898 | 
 899 | /**
 900 |  * Rename an existing tag
 901 |  * @param {string} tasksPath - Path to the tasks.json file
 902 |  * @param {string} oldName - Current name of the tag
 903 |  * @param {string} newName - New name for the tag
 904 |  * @param {Object} options - Options object
 905 |  * @param {Object} context - Context object containing session and projectRoot
 906 |  * @param {string} [context.projectRoot] - Project root path
 907 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
 908 |  * @param {string} outputFormat - Output format (text or json)
 909 |  * @returns {Promise<Object>} Result object with rename details
 910 |  */
 911 | async function renameTag(
 912 | 	tasksPath,
 913 | 	oldName,
 914 | 	newName,
 915 | 	options = {},
 916 | 	context = {},
 917 | 	outputFormat = 'text'
 918 | ) {
 919 | 	const { mcpLog, projectRoot } = context;
 920 | 
 921 | 	// Create a consistent logFn object regardless of context
 922 | 	const logFn = mcpLog || {
 923 | 		info: (...args) => log('info', ...args),
 924 | 		warn: (...args) => log('warn', ...args),
 925 | 		error: (...args) => log('error', ...args),
 926 | 		debug: (...args) => log('debug', ...args),
 927 | 		success: (...args) => log('success', ...args)
 928 | 	};
 929 | 
 930 | 	try {
 931 | 		// Validate parameters
 932 | 		if (!oldName || typeof oldName !== 'string') {
 933 | 			throw new Error('Old tag name is required and must be a string');
 934 | 		}
 935 | 		if (!newName || typeof newName !== 'string') {
 936 | 			throw new Error('New tag name is required and must be a string');
 937 | 		}
 938 | 
 939 | 		// Validate new tag name format
 940 | 		if (!/^[a-zA-Z0-9_-]+$/.test(newName)) {
 941 | 			throw new Error(
 942 | 				'New tag name can only contain letters, numbers, hyphens, and underscores'
 943 | 			);
 944 | 		}
 945 | 
 946 | 		// Prevent renaming master tag
 947 | 		if (oldName === 'master') {
 948 | 			throw new Error('Cannot rename the "master" tag');
 949 | 		}
 950 | 
 951 | 		// Reserved tag names
 952 | 		const reservedNames = ['master', 'main', 'default'];
 953 | 		if (reservedNames.includes(newName.toLowerCase())) {
 954 | 			throw new Error(`"${newName}" is a reserved tag name`);
 955 | 		}
 956 | 
 957 | 		logFn.info(`Renaming tag from "${oldName}" to "${newName}"`);
 958 | 
 959 | 		// Read current tasks data
 960 | 		const data = readJSON(tasksPath, projectRoot);
 961 | 		if (!data) {
 962 | 			throw new Error(`Could not read tasks file at ${tasksPath}`);
 963 | 		}
 964 | 
 965 | 		// Use raw tagged data for tag operations
 966 | 		const rawData = data._rawTaggedData || data;
 967 | 
 968 | 		// Check if old tag exists
 969 | 		if (!rawData[oldName]) {
 970 | 			throw new Error(`Tag "${oldName}" does not exist`);
 971 | 		}
 972 | 
 973 | 		// Check if new tag name already exists
 974 | 		if (rawData[newName]) {
 975 | 			throw new Error(`Tag "${newName}" already exists`);
 976 | 		}
 977 | 
 978 | 		// Get current tag to check if we're renaming the active tag
 979 | 		const currentTag = getCurrentTag(projectRoot);
 980 | 		const isCurrentTag = currentTag === oldName;
 981 | 
 982 | 		// Rename the tag by copying data and deleting old
 983 | 		rawData[newName] = { ...rawData[oldName] };
 984 | 
 985 | 		// Update metadata if it exists
 986 | 		if (rawData[newName].metadata) {
 987 | 			rawData[newName].metadata.renamed = {
 988 | 				from: oldName,
 989 | 				date: new Date().toISOString()
 990 | 			};
 991 | 		}
 992 | 
 993 | 		delete rawData[oldName];
 994 | 
 995 | 		// If we're renaming the current tag, update the current tag reference
 996 | 		if (isCurrentTag) {
 997 | 			await switchCurrentTag(projectRoot, newName);
 998 | 			logFn.info(`Updated current tag reference to "${newName}"`);
 999 | 		}
1000 | 
1001 | 		// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
1002 | 		const cleanData = {};
1003 | 		for (const [key, value] of Object.entries(rawData)) {
1004 | 			if (key !== '_rawTaggedData') {
1005 | 				cleanData[key] = value;
1006 | 			}
1007 | 		}
1008 | 
1009 | 		// Write the clean data back to file with proper context to avoid tag corruption
1010 | 		writeJSON(tasksPath, cleanData, projectRoot);
1011 | 
1012 | 		// Get task count
1013 | 		const tasks = getTasksForTag(rawData, newName);
1014 | 		const taskCount = tasks.length;
1015 | 
1016 | 		logFn.success(`Successfully renamed tag from "${oldName}" to "${newName}"`);
1017 | 
1018 | 		// For JSON output, return structured data
1019 | 		if (outputFormat === 'json') {
1020 | 			return {
1021 | 				oldName,
1022 | 				newName,
1023 | 				renamed: true,
1024 | 				taskCount,
1025 | 				wasCurrentTag: isCurrentTag,
1026 | 				isCurrentTag: isCurrentTag
1027 | 			};
1028 | 		}
1029 | 
1030 | 		// For text output, display success message
1031 | 		if (outputFormat === 'text') {
1032 | 			console.log(
1033 | 				boxen(
1034 | 					chalk.green.bold('✓ Tag Renamed Successfully') +
1035 | 						`\n\nOld Name: ${chalk.cyan(oldName)}` +
1036 | 						`\nNew Name: ${chalk.green.bold(newName)}` +
1037 | 						`\nTasks: ${chalk.yellow(taskCount)}` +
1038 | 						(isCurrentTag ? `\n${chalk.green('✓ Current tag updated')}` : ''),
1039 | 					{
1040 | 						padding: 1,
1041 | 						borderColor: 'green',
1042 | 						borderStyle: 'round',
1043 | 						margin: { top: 1, bottom: 1 }
1044 | 					}
1045 | 				)
1046 | 			);
1047 | 		}
1048 | 
1049 | 		return {
1050 | 			oldName,
1051 | 			newName,
1052 | 			renamed: true,
1053 | 			taskCount,
1054 | 			wasCurrentTag: isCurrentTag,
1055 | 			isCurrentTag: isCurrentTag
1056 | 		};
1057 | 	} catch (error) {
1058 | 		logFn.error(`Error renaming tag: ${error.message}`);
1059 | 		throw error;
1060 | 	}
1061 | }
1062 | 
1063 | /**
1064 |  * Copy an existing tag to create a new tag with the same tasks
1065 |  * @param {string} tasksPath - Path to the tasks.json file
1066 |  * @param {string} sourceName - Name of the source tag to copy from
1067 |  * @param {string} targetName - Name of the new tag to create
1068 |  * @param {Object} options - Options object
1069 |  * @param {string} [options.description] - Optional description for the new tag
1070 |  * @param {Object} context - Context object containing session and projectRoot
1071 |  * @param {string} [context.projectRoot] - Project root path
1072 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
1073 |  * @param {string} outputFormat - Output format (text or json)
1074 |  * @returns {Promise<Object>} Result object with copy details
1075 |  */
1076 | async function copyTag(
1077 | 	tasksPath,
1078 | 	sourceName,
1079 | 	targetName,
1080 | 	options = {},
1081 | 	context = {},
1082 | 	outputFormat = 'text'
1083 | ) {
1084 | 	const { mcpLog, projectRoot } = context;
1085 | 	const { description } = options;
1086 | 
1087 | 	// Create a consistent logFn object regardless of context
1088 | 	const logFn = mcpLog || {
1089 | 		info: (...args) => log('info', ...args),
1090 | 		warn: (...args) => log('warn', ...args),
1091 | 		error: (...args) => log('error', ...args),
1092 | 		debug: (...args) => log('debug', ...args),
1093 | 		success: (...args) => log('success', ...args)
1094 | 	};
1095 | 
1096 | 	try {
1097 | 		// Validate parameters
1098 | 		if (!sourceName || typeof sourceName !== 'string') {
1099 | 			throw new Error('Source tag name is required and must be a string');
1100 | 		}
1101 | 		if (!targetName || typeof targetName !== 'string') {
1102 | 			throw new Error('Target tag name is required and must be a string');
1103 | 		}
1104 | 
1105 | 		// Validate target tag name format
1106 | 		if (!/^[a-zA-Z0-9_-]+$/.test(targetName)) {
1107 | 			throw new Error(
1108 | 				'Target tag name can only contain letters, numbers, hyphens, and underscores'
1109 | 			);
1110 | 		}
1111 | 
1112 | 		// Reserved tag names
1113 | 		const reservedNames = ['master', 'main', 'default'];
1114 | 		if (reservedNames.includes(targetName.toLowerCase())) {
1115 | 			throw new Error(`"${targetName}" is a reserved tag name`);
1116 | 		}
1117 | 
1118 | 		logFn.info(`Copying tag from "${sourceName}" to "${targetName}"`);
1119 | 
1120 | 		// Read current tasks data
1121 | 		const data = readJSON(tasksPath, projectRoot);
1122 | 		if (!data) {
1123 | 			throw new Error(`Could not read tasks file at ${tasksPath}`);
1124 | 		}
1125 | 
1126 | 		// Use raw tagged data for tag operations
1127 | 		const rawData = data._rawTaggedData || data;
1128 | 
1129 | 		// Check if source tag exists
1130 | 		if (!rawData[sourceName]) {
1131 | 			throw new Error(`Source tag "${sourceName}" does not exist`);
1132 | 		}
1133 | 
1134 | 		// Check if target tag already exists
1135 | 		if (rawData[targetName]) {
1136 | 			throw new Error(`Target tag "${targetName}" already exists`);
1137 | 		}
1138 | 
1139 | 		// Get source tasks
1140 | 		const sourceTasks = getTasksForTag(rawData, sourceName);
1141 | 
1142 | 		// Create deep copy of the source tag data
1143 | 		rawData[targetName] = {
1144 | 			tasks: JSON.parse(JSON.stringify(sourceTasks)), // Deep copy tasks
1145 | 			metadata: {
1146 | 				created: new Date().toISOString(),
1147 | 				updated: new Date().toISOString(),
1148 | 				description:
1149 | 					description ||
1150 | 					`Copy of "${sourceName}" created on ${new Date().toLocaleDateString()}`,
1151 | 				copiedFrom: {
1152 | 					tag: sourceName,
1153 | 					date: new Date().toISOString()
1154 | 				}
1155 | 			}
1156 | 		};
1157 | 
1158 | 		// Create clean data for writing (exclude _rawTaggedData to prevent corruption)
1159 | 		const cleanData = {};
1160 | 		for (const [key, value] of Object.entries(rawData)) {
1161 | 			if (key !== '_rawTaggedData') {
1162 | 				cleanData[key] = value;
1163 | 			}
1164 | 		}
1165 | 
1166 | 		// Write the clean data back to file with proper context to avoid tag corruption
1167 | 		writeJSON(tasksPath, cleanData, projectRoot);
1168 | 
1169 | 		logFn.success(
1170 | 			`Successfully copied tag from "${sourceName}" to "${targetName}"`
1171 | 		);
1172 | 
1173 | 		// For JSON output, return structured data
1174 | 		if (outputFormat === 'json') {
1175 | 			return {
1176 | 				sourceName,
1177 | 				targetName,
1178 | 				copied: true,
1179 | 				description:
1180 | 					description ||
1181 | 					`Copy of "${sourceName}" created on ${new Date().toLocaleDateString()}`
1182 | 			};
1183 | 		}
1184 | 
1185 | 		// For text output, display success message
1186 | 		if (outputFormat === 'text') {
1187 | 			console.log(
1188 | 				boxen(
1189 | 					chalk.green.bold('✓ Tag Copied Successfully') +
1190 | 						`\n\nSource Tag: ${chalk.cyan(sourceName)}` +
1191 | 						`\nTarget Tag: ${chalk.green.bold(targetName)}` +
1192 | 						`\nTasks Copied: ${chalk.yellow(sourceTasks.length)}` +
1193 | 						(description ? `\nDescription: ${chalk.gray(description)}` : ''),
1194 | 					{
1195 | 						padding: 1,
1196 | 						borderColor: 'green',
1197 | 						borderStyle: 'round',
1198 | 						margin: { top: 1, bottom: 1 }
1199 | 					}
1200 | 				)
1201 | 			);
1202 | 		}
1203 | 
1204 | 		return {
1205 | 			sourceName,
1206 | 			targetName,
1207 | 			copied: true,
1208 | 			description:
1209 | 				description ||
1210 | 				`Copy of "${sourceName}" created on ${new Date().toLocaleDateString()}`
1211 | 		};
1212 | 	} catch (error) {
1213 | 		logFn.error(`Error copying tag: ${error.message}`);
1214 | 		throw error;
1215 | 	}
1216 | }
1217 | 
1218 | /**
1219 |  * Helper function to switch the current tag in state.json
1220 |  * @param {string} projectRoot - Project root directory
1221 |  * @param {string} tagName - Name of the tag to switch to
1222 |  * @returns {Promise<void>}
1223 |  */
1224 | async function switchCurrentTag(projectRoot, tagName) {
1225 | 	try {
1226 | 		const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
1227 | 
1228 | 		// Read current state or create default
1229 | 		let state = {};
1230 | 		if (fs.existsSync(statePath)) {
1231 | 			const rawState = fs.readFileSync(statePath, 'utf8');
1232 | 			state = JSON.parse(rawState);
1233 | 		}
1234 | 
1235 | 		// Update current tag and timestamp
1236 | 		state.currentTag = tagName;
1237 | 		state.lastSwitched = new Date().toISOString();
1238 | 
1239 | 		// Ensure other required state properties exist
1240 | 		if (!state.branchTagMapping) {
1241 | 			state.branchTagMapping = {};
1242 | 		}
1243 | 		if (state.migrationNoticeShown === undefined) {
1244 | 			state.migrationNoticeShown = false;
1245 | 		}
1246 | 
1247 | 		// Write updated state
1248 | 		fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
1249 | 	} catch (error) {
1250 | 		log('warn', `Could not update current tag in state.json: ${error.message}`);
1251 | 		// Don't throw - this is not critical for tag operations
1252 | 	}
1253 | }
1254 | 
1255 | /**
1256 |  * Update branch-tag mapping in state.json
1257 |  * @param {string} projectRoot - Project root directory
1258 |  * @param {string} branchName - Git branch name
1259 |  * @param {string} tagName - Tag name to map to
1260 |  * @returns {Promise<void>}
1261 |  */
1262 | async function updateBranchTagMapping(projectRoot, branchName, tagName) {
1263 | 	try {
1264 | 		const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
1265 | 
1266 | 		// Read current state or create default
1267 | 		let state = {};
1268 | 		if (fs.existsSync(statePath)) {
1269 | 			const rawState = fs.readFileSync(statePath, 'utf8');
1270 | 			state = JSON.parse(rawState);
1271 | 		}
1272 | 
1273 | 		// Ensure branchTagMapping exists
1274 | 		if (!state.branchTagMapping) {
1275 | 			state.branchTagMapping = {};
1276 | 		}
1277 | 
1278 | 		// Update the mapping
1279 | 		state.branchTagMapping[branchName] = tagName;
1280 | 
1281 | 		// Write updated state
1282 | 		fs.writeFileSync(statePath, JSON.stringify(state, null, 2), 'utf8');
1283 | 	} catch (error) {
1284 | 		log('warn', `Could not update branch-tag mapping: ${error.message}`);
1285 | 		// Don't throw - this is not critical for tag operations
1286 | 	}
1287 | }
1288 | 
1289 | /**
1290 |  * Get tag name for a git branch from state.json mapping
1291 |  * @param {string} projectRoot - Project root directory
1292 |  * @param {string} branchName - Git branch name
1293 |  * @returns {Promise<string|null>} Mapped tag name or null if not found
1294 |  */
1295 | async function getTagForBranch(projectRoot, branchName) {
1296 | 	try {
1297 | 		const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
1298 | 
1299 | 		if (!fs.existsSync(statePath)) {
1300 | 			return null;
1301 | 		}
1302 | 
1303 | 		const rawState = fs.readFileSync(statePath, 'utf8');
1304 | 		const state = JSON.parse(rawState);
1305 | 
1306 | 		return state.branchTagMapping?.[branchName] || null;
1307 | 	} catch (error) {
1308 | 		return null;
1309 | 	}
1310 | }
1311 | 
1312 | /**
1313 |  * Create a tag from a git branch name
1314 |  * @param {string} tasksPath - Path to the tasks.json file
1315 |  * @param {string} branchName - Git branch name to create tag from
1316 |  * @param {Object} options - Options object
1317 |  * @param {boolean} [options.copyFromCurrent] - Copy tasks from current tag
1318 |  * @param {string} [options.copyFromTag] - Copy tasks from specific tag
1319 |  * @param {string} [options.description] - Custom description for the tag
1320 |  * @param {boolean} [options.autoSwitch] - Automatically switch to the new tag
1321 |  * @param {Object} context - Context object containing session and projectRoot
1322 |  * @param {string} [context.projectRoot] - Project root path
1323 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
1324 |  * @param {string} outputFormat - Output format (text or json)
1325 |  * @returns {Promise<Object>} Result object with creation details
1326 |  */
1327 | async function createTagFromBranch(
1328 | 	tasksPath,
1329 | 	branchName,
1330 | 	options = {},
1331 | 	context = {},
1332 | 	outputFormat = 'text'
1333 | ) {
1334 | 	const { mcpLog, projectRoot } = context;
1335 | 	const { copyFromCurrent, copyFromTag, description, autoSwitch } = options;
1336 | 
1337 | 	// Import git utilities
1338 | 	const { sanitizeBranchNameForTag, isValidBranchForTag } = await import(
1339 | 		'../utils/git-utils.js'
1340 | 	);
1341 | 
1342 | 	// Create a consistent logFn object regardless of context
1343 | 	const logFn = mcpLog || {
1344 | 		info: (...args) => log('info', ...args),
1345 | 		warn: (...args) => log('warn', ...args),
1346 | 		error: (...args) => log('error', ...args),
1347 | 		debug: (...args) => log('debug', ...args),
1348 | 		success: (...args) => log('success', ...args)
1349 | 	};
1350 | 
1351 | 	try {
1352 | 		// Validate branch name
1353 | 		if (!branchName || typeof branchName !== 'string') {
1354 | 			throw new Error('Branch name is required and must be a string');
1355 | 		}
1356 | 
1357 | 		// Check if branch name is valid for tag creation
1358 | 		if (!isValidBranchForTag(branchName)) {
1359 | 			throw new Error(
1360 | 				`Branch "${branchName}" cannot be converted to a valid tag name`
1361 | 			);
1362 | 		}
1363 | 
1364 | 		// Sanitize branch name to create tag name
1365 | 		const tagName = sanitizeBranchNameForTag(branchName);
1366 | 
1367 | 		logFn.info(`Creating tag "${tagName}" from git branch "${branchName}"`);
1368 | 
1369 | 		// Create the tag using existing createTag function
1370 | 		const createResult = await createTag(
1371 | 			tasksPath,
1372 | 			tagName,
1373 | 			{
1374 | 				copyFromCurrent,
1375 | 				copyFromTag,
1376 | 				description:
1377 | 					description || `Tag created from git branch "${branchName}"`
1378 | 			},
1379 | 			context,
1380 | 			outputFormat
1381 | 		);
1382 | 
1383 | 		// Update branch-tag mapping
1384 | 		await updateBranchTagMapping(projectRoot, branchName, tagName);
1385 | 		logFn.info(`Updated branch-tag mapping: ${branchName} -> ${tagName}`);
1386 | 
1387 | 		// Auto-switch to the new tag if requested
1388 | 		if (autoSwitch) {
1389 | 			await switchCurrentTag(projectRoot, tagName);
1390 | 			logFn.info(`Automatically switched to tag "${tagName}"`);
1391 | 		}
1392 | 
1393 | 		// For JSON output, return structured data
1394 | 		if (outputFormat === 'json') {
1395 | 			return {
1396 | 				...createResult,
1397 | 				branchName,
1398 | 				tagName,
1399 | 				mappingUpdated: true,
1400 | 				autoSwitched: autoSwitch || false
1401 | 			};
1402 | 		}
1403 | 
1404 | 		// For text output, the createTag function already handles display
1405 | 		return {
1406 | 			branchName,
1407 | 			tagName,
1408 | 			created: true,
1409 | 			mappingUpdated: true,
1410 | 			autoSwitched: autoSwitch || false
1411 | 		};
1412 | 	} catch (error) {
1413 | 		logFn.error(`Error creating tag from branch: ${error.message}`);
1414 | 		throw error;
1415 | 	}
1416 | }
1417 | 
1418 | /**
1419 |  * Automatically switch tag based on current git branch
1420 |  * @param {string} tasksPath - Path to the tasks.json file
1421 |  * @param {Object} options - Options object
1422 |  * @param {boolean} [options.createIfMissing] - Create tag if it doesn't exist
1423 |  * @param {boolean} [options.copyFromCurrent] - Copy tasks when creating new tag
1424 |  * @param {Object} context - Context object containing session and projectRoot
1425 |  * @param {string} [context.projectRoot] - Project root path
1426 |  * @param {Object} [context.mcpLog] - MCP logger object (optional)
1427 |  * @param {string} outputFormat - Output format (text or json)
1428 |  * @returns {Promise<Object>} Result object with switch details
1429 |  */
1430 | async function autoSwitchTagForBranch(
1431 | 	tasksPath,
1432 | 	options = {},
1433 | 	context = {},
1434 | 	outputFormat = 'text'
1435 | ) {
1436 | 	const { mcpLog, projectRoot } = context;
1437 | 	const { createIfMissing, copyFromCurrent } = options;
1438 | 
1439 | 	// Import git utilities
1440 | 	const {
1441 | 		getCurrentBranch,
1442 | 		isGitRepository,
1443 | 		sanitizeBranchNameForTag,
1444 | 		isValidBranchForTag
1445 | 	} = await import('../utils/git-utils.js');
1446 | 
1447 | 	// Create a consistent logFn object regardless of context
1448 | 	const logFn = mcpLog || {
1449 | 		info: (...args) => log('info', ...args),
1450 | 		warn: (...args) => log('warn', ...args),
1451 | 		error: (...args) => log('error', ...args),
1452 | 		debug: (...args) => log('debug', ...args),
1453 | 		success: (...args) => log('success', ...args)
1454 | 	};
1455 | 
1456 | 	try {
1457 | 		// Check if we're in a git repository
1458 | 		if (!(await isGitRepository(projectRoot))) {
1459 | 			logFn.warn('Not in a git repository, cannot auto-switch tags');
1460 | 			return { switched: false, reason: 'not_git_repo' };
1461 | 		}
1462 | 
1463 | 		// Get current git branch
1464 | 		const currentBranch = await getCurrentBranch(projectRoot);
1465 | 		if (!currentBranch) {
1466 | 			logFn.warn('Could not determine current git branch');
1467 | 			return { switched: false, reason: 'no_current_branch' };
1468 | 		}
1469 | 
1470 | 		logFn.info(`Current git branch: ${currentBranch}`);
1471 | 
1472 | 		// Check if branch is valid for tag creation
1473 | 		if (!isValidBranchForTag(currentBranch)) {
1474 | 			logFn.info(`Branch "${currentBranch}" is not suitable for tag creation`);
1475 | 			return {
1476 | 				switched: false,
1477 | 				reason: 'invalid_branch_for_tag',
1478 | 				branchName: currentBranch
1479 | 			};
1480 | 		}
1481 | 
1482 | 		// Check if there's already a mapping for this branch
1483 | 		let tagName = await getTagForBranch(projectRoot, currentBranch);
1484 | 
1485 | 		if (!tagName) {
1486 | 			// No mapping exists, create tag name from branch
1487 | 			tagName = sanitizeBranchNameForTag(currentBranch);
1488 | 		}
1489 | 
1490 | 		// Check if tag exists
1491 | 		const data = readJSON(tasksPath, projectRoot);
1492 | 		const rawData = data._rawTaggedData || data;
1493 | 		const tagExists = rawData[tagName];
1494 | 
1495 | 		if (!tagExists && createIfMissing) {
1496 | 			// Create the tag from branch
1497 | 			logFn.info(`Creating new tag "${tagName}" for branch "${currentBranch}"`);
1498 | 
1499 | 			const createResult = await createTagFromBranch(
1500 | 				tasksPath,
1501 | 				currentBranch,
1502 | 				{
1503 | 					copyFromCurrent,
1504 | 					autoSwitch: true
1505 | 				},
1506 | 				context,
1507 | 				outputFormat
1508 | 			);
1509 | 
1510 | 			return {
1511 | 				switched: true,
1512 | 				created: true,
1513 | 				branchName: currentBranch,
1514 | 				tagName,
1515 | 				...createResult
1516 | 			};
1517 | 		} else if (tagExists) {
1518 | 			// Tag exists, switch to it
1519 | 			logFn.info(
1520 | 				`Switching to existing tag "${tagName}" for branch "${currentBranch}"`
1521 | 			);
1522 | 
1523 | 			const switchResult = await useTag(
1524 | 				tasksPath,
1525 | 				tagName,
1526 | 				{},
1527 | 				context,
1528 | 				outputFormat
1529 | 			);
1530 | 
1531 | 			// Update mapping if it didn't exist
1532 | 			if (!(await getTagForBranch(projectRoot, currentBranch))) {
1533 | 				await updateBranchTagMapping(projectRoot, currentBranch, tagName);
1534 | 			}
1535 | 
1536 | 			return {
1537 | 				switched: true,
1538 | 				created: false,
1539 | 				branchName: currentBranch,
1540 | 				tagName,
1541 | 				...switchResult
1542 | 			};
1543 | 		} else {
1544 | 			// Tag doesn't exist and createIfMissing is false
1545 | 			logFn.warn(
1546 | 				`Tag "${tagName}" for branch "${currentBranch}" does not exist`
1547 | 			);
1548 | 			return {
1549 | 				switched: false,
1550 | 				reason: 'tag_not_found',
1551 | 				branchName: currentBranch,
1552 | 				tagName
1553 | 			};
1554 | 		}
1555 | 	} catch (error) {
1556 | 		logFn.error(`Error in auto-switch tag for branch: ${error.message}`);
1557 | 		throw error;
1558 | 	}
1559 | }
1560 | 
1561 | /**
1562 |  * Check git workflow configuration and perform auto-switch if enabled
1563 |  * @param {string} projectRoot - Project root directory
1564 |  * @param {string} tasksPath - Path to the tasks.json file
1565 |  * @param {Object} context - Context object
1566 |  * @returns {Promise<Object|null>} Switch result or null if not enabled
1567 |  */
1568 | async function checkAndAutoSwitchTag(projectRoot, tasksPath, context = {}) {
1569 | 	try {
1570 | 		// Read configuration
1571 | 		const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
1572 | 		if (!fs.existsSync(configPath)) {
1573 | 			return null;
1574 | 		}
1575 | 
1576 | 		const rawConfig = fs.readFileSync(configPath, 'utf8');
1577 | 		const config = JSON.parse(rawConfig);
1578 | 
1579 | 		// Git workflow has been removed - return null to disable auto-switching
1580 | 		return null;
1581 | 
1582 | 		// Perform auto-switch
1583 | 		return await autoSwitchTagForBranch(
1584 | 			tasksPath,
1585 | 			{ createIfMissing: true, copyFromCurrent: false },
1586 | 			context,
1587 | 			'json'
1588 | 		);
1589 | 	} catch (error) {
1590 | 		// Silently fail - this is not critical
1591 | 		return null;
1592 | 	}
1593 | }
1594 | 
1595 | // Export all tag management functions
1596 | export {
1597 | 	createTag,
1598 | 	deleteTag,
1599 | 	tags,
1600 | 	useTag,
1601 | 	renameTag,
1602 | 	copyTag,
1603 | 	switchCurrentTag,
1604 | 	updateBranchTagMapping,
1605 | 	getTagForBranch,
1606 | 	createTagFromBranch,
1607 | 	autoSwitchTagForBranch,
1608 | 	checkAndAutoSwitchTag
1609 | };
1610 | 
```
Page 59/69FirstPrevNextLast