#
tokens: 44132/50000 2/821 files (page 43/52)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 43 of 52. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .changeset
│   ├── config.json
│   └── README.md
├── .claude
│   ├── agents
│   │   ├── task-checker.md
│   │   ├── task-executor.md
│   │   └── task-orchestrator.md
│   ├── commands
│   │   ├── dedupe.md
│   │   └── tm
│   │       ├── add-dependency
│   │       │   └── add-dependency.md
│   │       ├── add-subtask
│   │       │   ├── add-subtask.md
│   │       │   └── convert-task-to-subtask.md
│   │       ├── add-task
│   │       │   └── add-task.md
│   │       ├── analyze-complexity
│   │       │   └── analyze-complexity.md
│   │       ├── complexity-report
│   │       │   └── complexity-report.md
│   │       ├── expand
│   │       │   ├── expand-all-tasks.md
│   │       │   └── expand-task.md
│   │       ├── fix-dependencies
│   │       │   └── fix-dependencies.md
│   │       ├── generate
│   │       │   └── generate-tasks.md
│   │       ├── help.md
│   │       ├── init
│   │       │   ├── init-project-quick.md
│   │       │   └── init-project.md
│   │       ├── learn.md
│   │       ├── list
│   │       │   ├── list-tasks-by-status.md
│   │       │   ├── list-tasks-with-subtasks.md
│   │       │   └── list-tasks.md
│   │       ├── models
│   │       │   ├── setup-models.md
│   │       │   └── view-models.md
│   │       ├── next
│   │       │   └── next-task.md
│   │       ├── parse-prd
│   │       │   ├── parse-prd-with-research.md
│   │       │   └── parse-prd.md
│   │       ├── remove-dependency
│   │       │   └── remove-dependency.md
│   │       ├── remove-subtask
│   │       │   └── remove-subtask.md
│   │       ├── remove-subtasks
│   │       │   ├── remove-all-subtasks.md
│   │       │   └── remove-subtasks.md
│   │       ├── remove-task
│   │       │   └── remove-task.md
│   │       ├── set-status
│   │       │   ├── to-cancelled.md
│   │       │   ├── to-deferred.md
│   │       │   ├── to-done.md
│   │       │   ├── to-in-progress.md
│   │       │   ├── to-pending.md
│   │       │   └── to-review.md
│   │       ├── setup
│   │       │   ├── install-taskmaster.md
│   │       │   └── quick-install-taskmaster.md
│   │       ├── show
│   │       │   └── show-task.md
│   │       ├── status
│   │       │   └── project-status.md
│   │       ├── sync-readme
│   │       │   └── sync-readme.md
│   │       ├── tm-main.md
│   │       ├── update
│   │       │   ├── update-single-task.md
│   │       │   ├── update-task.md
│   │       │   └── update-tasks-from-id.md
│   │       ├── utils
│   │       │   └── analyze-project.md
│   │       ├── validate-dependencies
│   │       │   └── validate-dependencies.md
│   │       └── workflows
│   │           ├── auto-implement-tasks.md
│   │           ├── command-pipeline.md
│   │           └── smart-workflow.md
│   └── TM_COMMANDS_GUIDE.md
├── .coderabbit.yaml
├── .cursor
│   ├── mcp.json
│   └── rules
│       ├── ai_providers.mdc
│       ├── ai_services.mdc
│       ├── architecture.mdc
│       ├── changeset.mdc
│       ├── commands.mdc
│       ├── context_gathering.mdc
│       ├── cursor_rules.mdc
│       ├── dependencies.mdc
│       ├── dev_workflow.mdc
│       ├── git_workflow.mdc
│       ├── glossary.mdc
│       ├── mcp.mdc
│       ├── new_features.mdc
│       ├── self_improve.mdc
│       ├── tags.mdc
│       ├── taskmaster.mdc
│       ├── tasks.mdc
│       ├── telemetry.mdc
│       ├── test_workflow.mdc
│       ├── tests.mdc
│       ├── ui.mdc
│       └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.md
│   │   ├── enhancements---feature-requests.md
│   │   └── feedback.md
│   ├── PULL_REQUEST_TEMPLATE
│   │   ├── bugfix.md
│   │   ├── config.yml
│   │   ├── feature.md
│   │   └── integration.md
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── scripts
│   │   ├── auto-close-duplicates.mjs
│   │   ├── backfill-duplicate-comments.mjs
│   │   ├── check-pre-release-mode.mjs
│   │   ├── parse-metrics.mjs
│   │   ├── release.mjs
│   │   ├── tag-extension.mjs
│   │   └── utils.mjs
│   └── workflows
│       ├── auto-close-duplicates.yml
│       ├── backfill-duplicate-comments.yml
│       ├── ci.yml
│       ├── claude-dedupe-issues.yml
│       ├── claude-docs-trigger.yml
│       ├── claude-docs-updater.yml
│       ├── claude-issue-triage.yml
│       ├── claude.yml
│       ├── extension-ci.yml
│       ├── extension-release.yml
│       ├── log-issue-events.yml
│       ├── pre-release.yml
│       ├── release-check.yml
│       ├── release.yml
│       ├── update-models-md.yml
│       └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│   ├── hooks
│   │   ├── tm-code-change-task-tracker.kiro.hook
│   │   ├── tm-complexity-analyzer.kiro.hook
│   │   ├── tm-daily-standup-assistant.kiro.hook
│   │   ├── tm-git-commit-task-linker.kiro.hook
│   │   ├── tm-pr-readiness-checker.kiro.hook
│   │   ├── tm-task-dependency-auto-progression.kiro.hook
│   │   └── tm-test-success-task-completer.kiro.hook
│   ├── settings
│   │   └── mcp.json
│   └── steering
│       ├── dev_workflow.md
│       ├── kiro_rules.md
│       ├── self_improve.md
│       ├── taskmaster_hooks_workflow.md
│       └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│   ├── CLAUDE.md
│   ├── config.json
│   ├── docs
│   │   ├── MIGRATION-ROADMAP.md
│   │   ├── prd-tm-start.txt
│   │   ├── prd.txt
│   │   ├── README.md
│   │   ├── research
│   │   │   ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│   │   │   ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│   │   │   ├── 2025-06-14_test-save-functionality.md
│   │   │   ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│   │   │   └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│   │   ├── task-template-importing-prd.txt
│   │   ├── test-prd.txt
│   │   └── tm-core-phase-1.txt
│   ├── reports
│   │   ├── task-complexity-report_cc-kiro-hooks.json
│   │   ├── task-complexity-report_test-prd-tag.json
│   │   ├── task-complexity-report_tm-core-phase-1.json
│   │   ├── task-complexity-report.json
│   │   └── tm-core-complexity.json
│   ├── state.json
│   ├── tasks
│   │   ├── task_001_tm-start.txt
│   │   ├── task_002_tm-start.txt
│   │   ├── task_003_tm-start.txt
│   │   ├── task_004_tm-start.txt
│   │   ├── task_007_tm-start.txt
│   │   └── tasks.json
│   └── templates
│       └── example_prd.txt
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── apps
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── auth.command.ts
│   │   │   │   ├── context.command.ts
│   │   │   │   ├── list.command.ts
│   │   │   │   ├── set-status.command.ts
│   │   │   │   ├── show.command.ts
│   │   │   │   └── start.command.ts
│   │   │   ├── index.ts
│   │   │   ├── ui
│   │   │   │   ├── components
│   │   │   │   │   ├── dashboard.component.ts
│   │   │   │   │   ├── header.component.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next-task.component.ts
│   │   │   │   │   ├── suggested-steps.component.ts
│   │   │   │   │   └── task-detail.component.ts
│   │   │   │   └── index.ts
│   │   │   └── utils
│   │   │       ├── auto-update.ts
│   │   │       └── ui.ts
│   │   └── tsconfig.json
│   ├── docs
│   │   ├── archive
│   │   │   ├── ai-client-utils-example.mdx
│   │   │   ├── ai-development-workflow.mdx
│   │   │   ├── command-reference.mdx
│   │   │   ├── configuration.mdx
│   │   │   ├── cursor-setup.mdx
│   │   │   ├── examples.mdx
│   │   │   └── Installation.mdx
│   │   ├── best-practices
│   │   │   ├── advanced-tasks.mdx
│   │   │   ├── configuration-advanced.mdx
│   │   │   └── index.mdx
│   │   ├── capabilities
│   │   │   ├── cli-root-commands.mdx
│   │   │   ├── index.mdx
│   │   │   ├── mcp.mdx
│   │   │   └── task-structure.mdx
│   │   ├── CHANGELOG.md
│   │   ├── docs.json
│   │   ├── favicon.svg
│   │   ├── getting-started
│   │   │   ├── contribute.mdx
│   │   │   ├── faq.mdx
│   │   │   └── quick-start
│   │   │       ├── configuration-quick.mdx
│   │   │       ├── execute-quick.mdx
│   │   │       ├── installation.mdx
│   │   │       ├── moving-forward.mdx
│   │   │       ├── prd-quick.mdx
│   │   │       ├── quick-start.mdx
│   │   │       ├── requirements.mdx
│   │   │       ├── rules-quick.mdx
│   │   │       └── tasks-quick.mdx
│   │   ├── introduction.mdx
│   │   ├── licensing.md
│   │   ├── logo
│   │   │   ├── dark.svg
│   │   │   ├── light.svg
│   │   │   └── task-master-logo.png
│   │   ├── package.json
│   │   ├── README.md
│   │   ├── style.css
│   │   ├── vercel.json
│   │   └── whats-new.mdx
│   └── extension
│       ├── .vscodeignore
│       ├── assets
│       │   ├── banner.png
│       │   ├── icon-dark.svg
│       │   ├── icon-light.svg
│       │   ├── icon.png
│       │   ├── screenshots
│       │   │   ├── kanban-board.png
│       │   │   └── task-details.png
│       │   └── sidebar-icon.svg
│       ├── CHANGELOG.md
│       ├── components.json
│       ├── docs
│       │   ├── extension-CI-setup.md
│       │   └── extension-development-guide.md
│       ├── esbuild.js
│       ├── LICENSE
│       ├── package.json
│       ├── package.mjs
│       ├── package.publish.json
│       ├── README.md
│       ├── src
│       │   ├── components
│       │   │   ├── ConfigView.tsx
│       │   │   ├── constants.ts
│       │   │   ├── TaskDetails
│       │   │   │   ├── AIActionsSection.tsx
│       │   │   │   ├── DetailsSection.tsx
│       │   │   │   ├── PriorityBadge.tsx
│       │   │   │   ├── SubtasksSection.tsx
│       │   │   │   ├── TaskMetadataSidebar.tsx
│       │   │   │   └── useTaskDetails.ts
│       │   │   ├── TaskDetailsView.tsx
│       │   │   ├── TaskMasterLogo.tsx
│       │   │   └── ui
│       │   │       ├── badge.tsx
│       │   │       ├── breadcrumb.tsx
│       │   │       ├── button.tsx
│       │   │       ├── card.tsx
│       │   │       ├── collapsible.tsx
│       │   │       ├── CollapsibleSection.tsx
│       │   │       ├── dropdown-menu.tsx
│       │   │       ├── label.tsx
│       │   │       ├── scroll-area.tsx
│       │   │       ├── separator.tsx
│       │   │       ├── shadcn-io
│       │   │       │   └── kanban
│       │   │       │       └── index.tsx
│       │   │       └── textarea.tsx
│       │   ├── extension.ts
│       │   ├── index.ts
│       │   ├── lib
│       │   │   └── utils.ts
│       │   ├── services
│       │   │   ├── config-service.ts
│       │   │   ├── error-handler.ts
│       │   │   ├── notification-preferences.ts
│       │   │   ├── polling-service.ts
│       │   │   ├── polling-strategies.ts
│       │   │   ├── sidebar-webview-manager.ts
│       │   │   ├── task-repository.ts
│       │   │   ├── terminal-manager.ts
│       │   │   └── webview-manager.ts
│       │   ├── test
│       │   │   └── extension.test.ts
│       │   ├── utils
│       │   │   ├── configManager.ts
│       │   │   ├── connectionManager.ts
│       │   │   ├── errorHandler.ts
│       │   │   ├── event-emitter.ts
│       │   │   ├── logger.ts
│       │   │   ├── mcpClient.ts
│       │   │   ├── notificationPreferences.ts
│       │   │   └── task-master-api
│       │   │       ├── cache
│       │   │       │   └── cache-manager.ts
│       │   │       ├── index.ts
│       │   │       ├── mcp-client.ts
│       │   │       ├── transformers
│       │   │       │   └── task-transformer.ts
│       │   │       └── types
│       │   │           └── index.ts
│       │   └── webview
│       │       ├── App.tsx
│       │       ├── components
│       │       │   ├── AppContent.tsx
│       │       │   ├── EmptyState.tsx
│       │       │   ├── ErrorBoundary.tsx
│       │       │   ├── PollingStatus.tsx
│       │       │   ├── PriorityBadge.tsx
│       │       │   ├── SidebarView.tsx
│       │       │   ├── TagDropdown.tsx
│       │       │   ├── TaskCard.tsx
│       │       │   ├── TaskEditModal.tsx
│       │       │   ├── TaskMasterKanban.tsx
│       │       │   ├── ToastContainer.tsx
│       │       │   └── ToastNotification.tsx
│       │       ├── constants
│       │       │   └── index.ts
│       │       ├── contexts
│       │       │   └── VSCodeContext.tsx
│       │       ├── hooks
│       │       │   ├── useTaskQueries.ts
│       │       │   ├── useVSCodeMessages.ts
│       │       │   └── useWebviewHeight.ts
│       │       ├── index.css
│       │       ├── index.tsx
│       │       ├── providers
│       │       │   └── QueryProvider.tsx
│       │       ├── reducers
│       │       │   └── appReducer.ts
│       │       ├── sidebar.tsx
│       │       ├── types
│       │       │   └── index.ts
│       │       └── utils
│       │           ├── logger.ts
│       │           └── toast.ts
│       └── tsconfig.json
├── assets
│   ├── .windsurfrules
│   ├── AGENTS.md
│   ├── claude
│   │   ├── agents
│   │   │   ├── task-checker.md
│   │   │   ├── task-executor.md
│   │   │   └── task-orchestrator.md
│   │   ├── commands
│   │   │   └── tm
│   │   │       ├── add-dependency
│   │   │       │   └── add-dependency.md
│   │   │       ├── add-subtask
│   │   │       │   ├── add-subtask.md
│   │   │       │   └── convert-task-to-subtask.md
│   │   │       ├── add-task
│   │   │       │   └── add-task.md
│   │   │       ├── analyze-complexity
│   │   │       │   └── analyze-complexity.md
│   │   │       ├── clear-subtasks
│   │   │       │   ├── clear-all-subtasks.md
│   │   │       │   └── clear-subtasks.md
│   │   │       ├── complexity-report
│   │   │       │   └── complexity-report.md
│   │   │       ├── expand
│   │   │       │   ├── expand-all-tasks.md
│   │   │       │   └── expand-task.md
│   │   │       ├── fix-dependencies
│   │   │       │   └── fix-dependencies.md
│   │   │       ├── generate
│   │   │       │   └── generate-tasks.md
│   │   │       ├── help.md
│   │   │       ├── init
│   │   │       │   ├── init-project-quick.md
│   │   │       │   └── init-project.md
│   │   │       ├── learn.md
│   │   │       ├── list
│   │   │       │   ├── list-tasks-by-status.md
│   │   │       │   ├── list-tasks-with-subtasks.md
│   │   │       │   └── list-tasks.md
│   │   │       ├── models
│   │   │       │   ├── setup-models.md
│   │   │       │   └── view-models.md
│   │   │       ├── next
│   │   │       │   └── next-task.md
│   │   │       ├── parse-prd
│   │   │       │   ├── parse-prd-with-research.md
│   │   │       │   └── parse-prd.md
│   │   │       ├── remove-dependency
│   │   │       │   └── remove-dependency.md
│   │   │       ├── remove-subtask
│   │   │       │   └── remove-subtask.md
│   │   │       ├── remove-subtasks
│   │   │       │   ├── remove-all-subtasks.md
│   │   │       │   └── remove-subtasks.md
│   │   │       ├── remove-task
│   │   │       │   └── remove-task.md
│   │   │       ├── set-status
│   │   │       │   ├── to-cancelled.md
│   │   │       │   ├── to-deferred.md
│   │   │       │   ├── to-done.md
│   │   │       │   ├── to-in-progress.md
│   │   │       │   ├── to-pending.md
│   │   │       │   └── to-review.md
│   │   │       ├── setup
│   │   │       │   ├── install-taskmaster.md
│   │   │       │   └── quick-install-taskmaster.md
│   │   │       ├── show
│   │   │       │   └── show-task.md
│   │   │       ├── status
│   │   │       │   └── project-status.md
│   │   │       ├── sync-readme
│   │   │       │   └── sync-readme.md
│   │   │       ├── tm-main.md
│   │   │       ├── update
│   │   │       │   ├── update-single-task.md
│   │   │       │   ├── update-task.md
│   │   │       │   └── update-tasks-from-id.md
│   │   │       ├── utils
│   │   │       │   └── analyze-project.md
│   │   │       ├── validate-dependencies
│   │   │       │   └── validate-dependencies.md
│   │   │       └── workflows
│   │   │           ├── auto-implement-tasks.md
│   │   │           ├── command-pipeline.md
│   │   │           └── smart-workflow.md
│   │   └── TM_COMMANDS_GUIDE.md
│   ├── config.json
│   ├── env.example
│   ├── example_prd.txt
│   ├── gitignore
│   ├── kiro-hooks
│   │   ├── tm-code-change-task-tracker.kiro.hook
│   │   ├── tm-complexity-analyzer.kiro.hook
│   │   ├── tm-daily-standup-assistant.kiro.hook
│   │   ├── tm-git-commit-task-linker.kiro.hook
│   │   ├── tm-pr-readiness-checker.kiro.hook
│   │   ├── tm-task-dependency-auto-progression.kiro.hook
│   │   └── tm-test-success-task-completer.kiro.hook
│   ├── roocode
│   │   ├── .roo
│   │   │   ├── rules-architect
│   │   │   │   └── architect-rules
│   │   │   ├── rules-ask
│   │   │   │   └── ask-rules
│   │   │   ├── rules-code
│   │   │   │   └── code-rules
│   │   │   ├── rules-debug
│   │   │   │   └── debug-rules
│   │   │   ├── rules-orchestrator
│   │   │   │   └── orchestrator-rules
│   │   │   └── rules-test
│   │   │       └── test-rules
│   │   └── .roomodes
│   ├── rules
│   │   ├── cursor_rules.mdc
│   │   ├── dev_workflow.mdc
│   │   ├── self_improve.mdc
│   │   ├── taskmaster_hooks_workflow.mdc
│   │   └── taskmaster.mdc
│   └── scripts_README.md
├── bin
│   └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE.md
├── context
│   ├── chats
│   │   ├── add-task-dependencies-1.md
│   │   └── max-min-tokens.txt.md
│   ├── fastmcp-core.txt
│   ├── fastmcp-docs.txt
│   ├── MCP_INTEGRATION.md
│   ├── mcp-js-sdk-docs.txt
│   ├── mcp-protocol-repo.txt
│   ├── mcp-protocol-schema-03262025.json
│   └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│   ├── CLI-COMMANDER-PATTERN.md
│   ├── command-reference.md
│   ├── configuration.md
│   ├── contributor-docs
│   │   └── testing-roo-integration.md
│   ├── cross-tag-task-movement.md
│   ├── examples
│   │   └── claude-code-usage.md
│   ├── examples.md
│   ├── licensing.md
│   ├── mcp-provider-guide.md
│   ├── mcp-provider.md
│   ├── migration-guide.md
│   ├── models.md
│   ├── providers
│   │   └── gemini-cli.md
│   ├── README.md
│   ├── scripts
│   │   └── models-json-to-markdown.js
│   ├── task-structure.md
│   └── tutorial.md
├── images
│   └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│   ├── server.js
│   └── src
│       ├── core
│       │   ├── __tests__
│       │   │   └── context-manager.test.js
│       │   ├── context-manager.js
│       │   ├── direct-functions
│       │   │   ├── add-dependency.js
│       │   │   ├── add-subtask.js
│       │   │   ├── add-tag.js
│       │   │   ├── add-task.js
│       │   │   ├── analyze-task-complexity.js
│       │   │   ├── cache-stats.js
│       │   │   ├── clear-subtasks.js
│       │   │   ├── complexity-report.js
│       │   │   ├── copy-tag.js
│       │   │   ├── create-tag-from-branch.js
│       │   │   ├── delete-tag.js
│       │   │   ├── expand-all-tasks.js
│       │   │   ├── expand-task.js
│       │   │   ├── fix-dependencies.js
│       │   │   ├── generate-task-files.js
│       │   │   ├── initialize-project.js
│       │   │   ├── list-tags.js
│       │   │   ├── list-tasks.js
│       │   │   ├── models.js
│       │   │   ├── move-task-cross-tag.js
│       │   │   ├── move-task.js
│       │   │   ├── next-task.js
│       │   │   ├── parse-prd.js
│       │   │   ├── remove-dependency.js
│       │   │   ├── remove-subtask.js
│       │   │   ├── remove-task.js
│       │   │   ├── rename-tag.js
│       │   │   ├── research.js
│       │   │   ├── response-language.js
│       │   │   ├── rules.js
│       │   │   ├── scope-down.js
│       │   │   ├── scope-up.js
│       │   │   ├── set-task-status.js
│       │   │   ├── show-task.js
│       │   │   ├── update-subtask-by-id.js
│       │   │   ├── update-task-by-id.js
│       │   │   ├── update-tasks.js
│       │   │   ├── use-tag.js
│       │   │   └── validate-dependencies.js
│       │   ├── task-master-core.js
│       │   └── utils
│       │       ├── env-utils.js
│       │       └── path-utils.js
│       ├── custom-sdk
│       │   ├── errors.js
│       │   ├── index.js
│       │   ├── json-extractor.js
│       │   ├── language-model.js
│       │   ├── message-converter.js
│       │   └── schema-converter.js
│       ├── index.js
│       ├── logger.js
│       ├── providers
│       │   └── mcp-provider.js
│       └── tools
│           ├── add-dependency.js
│           ├── add-subtask.js
│           ├── add-tag.js
│           ├── add-task.js
│           ├── analyze.js
│           ├── clear-subtasks.js
│           ├── complexity-report.js
│           ├── copy-tag.js
│           ├── delete-tag.js
│           ├── expand-all.js
│           ├── expand-task.js
│           ├── fix-dependencies.js
│           ├── generate.js
│           ├── get-operation-status.js
│           ├── get-task.js
│           ├── get-tasks.js
│           ├── index.js
│           ├── initialize-project.js
│           ├── list-tags.js
│           ├── models.js
│           ├── move-task.js
│           ├── next-task.js
│           ├── parse-prd.js
│           ├── remove-dependency.js
│           ├── remove-subtask.js
│           ├── remove-task.js
│           ├── rename-tag.js
│           ├── research.js
│           ├── response-language.js
│           ├── rules.js
│           ├── scope-down.js
│           ├── scope-up.js
│           ├── set-task-status.js
│           ├── update-subtask.js
│           ├── update-task.js
│           ├── update.js
│           ├── use-tag.js
│           ├── utils.js
│           └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│   ├── build-config
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   └── tsdown.base.ts
│   │   └── tsconfig.json
│   └── tm-core
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── docs
│       │   └── listTasks-architecture.md
│       ├── package.json
│       ├── POC-STATUS.md
│       ├── README.md
│       ├── src
│       │   ├── auth
│       │   │   ├── auth-manager.test.ts
│       │   │   ├── auth-manager.ts
│       │   │   ├── config.ts
│       │   │   ├── credential-store.test.ts
│       │   │   ├── credential-store.ts
│       │   │   ├── index.ts
│       │   │   ├── oauth-service.ts
│       │   │   ├── supabase-session-storage.ts
│       │   │   └── types.ts
│       │   ├── clients
│       │   │   ├── index.ts
│       │   │   └── supabase-client.ts
│       │   ├── config
│       │   │   ├── config-manager.spec.ts
│       │   │   ├── config-manager.ts
│       │   │   ├── index.ts
│       │   │   └── services
│       │   │       ├── config-loader.service.spec.ts
│       │   │       ├── config-loader.service.ts
│       │   │       ├── config-merger.service.spec.ts
│       │   │       ├── config-merger.service.ts
│       │   │       ├── config-persistence.service.spec.ts
│       │   │       ├── config-persistence.service.ts
│       │   │       ├── environment-config-provider.service.spec.ts
│       │   │       ├── environment-config-provider.service.ts
│       │   │       ├── index.ts
│       │   │       ├── runtime-state-manager.service.spec.ts
│       │   │       └── runtime-state-manager.service.ts
│       │   ├── constants
│       │   │   └── index.ts
│       │   ├── entities
│       │   │   └── task.entity.ts
│       │   ├── errors
│       │   │   ├── index.ts
│       │   │   └── task-master-error.ts
│       │   ├── executors
│       │   │   ├── base-executor.ts
│       │   │   ├── claude-executor.ts
│       │   │   ├── executor-factory.ts
│       │   │   ├── executor-service.ts
│       │   │   ├── index.ts
│       │   │   └── types.ts
│       │   ├── index.ts
│       │   ├── interfaces
│       │   │   ├── ai-provider.interface.ts
│       │   │   ├── configuration.interface.ts
│       │   │   ├── index.ts
│       │   │   └── storage.interface.ts
│       │   ├── logger
│       │   │   ├── factory.ts
│       │   │   ├── index.ts
│       │   │   └── logger.ts
│       │   ├── mappers
│       │   │   └── TaskMapper.ts
│       │   ├── parser
│       │   │   └── index.ts
│       │   ├── providers
│       │   │   ├── ai
│       │   │   │   ├── base-provider.ts
│       │   │   │   └── index.ts
│       │   │   └── index.ts
│       │   ├── repositories
│       │   │   ├── supabase-task-repository.ts
│       │   │   └── task-repository.interface.ts
│       │   ├── services
│       │   │   ├── index.ts
│       │   │   ├── organization.service.ts
│       │   │   ├── task-execution-service.ts
│       │   │   └── task-service.ts
│       │   ├── storage
│       │   │   ├── api-storage.ts
│       │   │   ├── file-storage
│       │   │   │   ├── file-operations.ts
│       │   │   │   ├── file-storage.ts
│       │   │   │   ├── format-handler.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── path-resolver.ts
│       │   │   ├── index.ts
│       │   │   └── storage-factory.ts
│       │   ├── subpath-exports.test.ts
│       │   ├── task-master-core.ts
│       │   ├── types
│       │   │   ├── database.types.ts
│       │   │   ├── index.ts
│       │   │   └── legacy.ts
│       │   └── utils
│       │       ├── id-generator.ts
│       │       └── index.ts
│       ├── tests
│       │   ├── integration
│       │   │   └── list-tasks.test.ts
│       │   ├── mocks
│       │   │   └── mock-provider.ts
│       │   ├── setup.ts
│       │   └── unit
│       │       ├── base-provider.test.ts
│       │       ├── executor.test.ts
│       │       └── smoke.test.ts
│       ├── tsconfig.json
│       └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│   ├── dev.js
│   ├── init.js
│   ├── modules
│   │   ├── ai-services-unified.js
│   │   ├── commands.js
│   │   ├── config-manager.js
│   │   ├── dependency-manager.js
│   │   ├── index.js
│   │   ├── prompt-manager.js
│   │   ├── supported-models.json
│   │   ├── sync-readme.js
│   │   ├── task-manager
│   │   │   ├── add-subtask.js
│   │   │   ├── add-task.js
│   │   │   ├── analyze-task-complexity.js
│   │   │   ├── clear-subtasks.js
│   │   │   ├── expand-all-tasks.js
│   │   │   ├── expand-task.js
│   │   │   ├── find-next-task.js
│   │   │   ├── generate-task-files.js
│   │   │   ├── is-task-dependent.js
│   │   │   ├── list-tasks.js
│   │   │   ├── migrate.js
│   │   │   ├── models.js
│   │   │   ├── move-task.js
│   │   │   ├── parse-prd
│   │   │   │   ├── index.js
│   │   │   │   ├── parse-prd-config.js
│   │   │   │   ├── parse-prd-helpers.js
│   │   │   │   ├── parse-prd-non-streaming.js
│   │   │   │   ├── parse-prd-streaming.js
│   │   │   │   └── parse-prd.js
│   │   │   ├── remove-subtask.js
│   │   │   ├── remove-task.js
│   │   │   ├── research.js
│   │   │   ├── response-language.js
│   │   │   ├── scope-adjustment.js
│   │   │   ├── set-task-status.js
│   │   │   ├── tag-management.js
│   │   │   ├── task-exists.js
│   │   │   ├── update-single-task-status.js
│   │   │   ├── update-subtask-by-id.js
│   │   │   ├── update-task-by-id.js
│   │   │   └── update-tasks.js
│   │   ├── task-manager.js
│   │   ├── ui.js
│   │   ├── update-config-tokens.js
│   │   ├── utils
│   │   │   ├── contextGatherer.js
│   │   │   ├── fuzzyTaskSearch.js
│   │   │   └── git-utils.js
│   │   └── utils.js
│   ├── task-complexity-report.json
│   ├── test-claude-errors.js
│   └── test-claude.js
├── src
│   ├── ai-providers
│   │   ├── anthropic.js
│   │   ├── azure.js
│   │   ├── base-provider.js
│   │   ├── bedrock.js
│   │   ├── claude-code.js
│   │   ├── custom-sdk
│   │   │   ├── claude-code
│   │   │   │   ├── errors.js
│   │   │   │   ├── index.js
│   │   │   │   ├── json-extractor.js
│   │   │   │   ├── language-model.js
│   │   │   │   ├── message-converter.js
│   │   │   │   └── types.js
│   │   │   └── grok-cli
│   │   │       ├── errors.js
│   │   │       ├── index.js
│   │   │       ├── json-extractor.js
│   │   │       ├── language-model.js
│   │   │       ├── message-converter.js
│   │   │       └── types.js
│   │   ├── gemini-cli.js
│   │   ├── google-vertex.js
│   │   ├── google.js
│   │   ├── grok-cli.js
│   │   ├── groq.js
│   │   ├── index.js
│   │   ├── ollama.js
│   │   ├── openai.js
│   │   ├── openrouter.js
│   │   ├── perplexity.js
│   │   └── xai.js
│   ├── constants
│   │   ├── commands.js
│   │   ├── paths.js
│   │   ├── profiles.js
│   │   ├── providers.js
│   │   ├── rules-actions.js
│   │   ├── task-priority.js
│   │   └── task-status.js
│   ├── profiles
│   │   ├── amp.js
│   │   ├── base-profile.js
│   │   ├── claude.js
│   │   ├── cline.js
│   │   ├── codex.js
│   │   ├── cursor.js
│   │   ├── gemini.js
│   │   ├── index.js
│   │   ├── kilo.js
│   │   ├── kiro.js
│   │   ├── opencode.js
│   │   ├── roo.js
│   │   ├── trae.js
│   │   ├── vscode.js
│   │   ├── windsurf.js
│   │   └── zed.js
│   ├── progress
│   │   ├── base-progress-tracker.js
│   │   ├── cli-progress-factory.js
│   │   ├── parse-prd-tracker.js
│   │   ├── progress-tracker-builder.js
│   │   └── tracker-ui.js
│   ├── prompts
│   │   ├── add-task.json
│   │   ├── analyze-complexity.json
│   │   ├── expand-task.json
│   │   ├── parse-prd.json
│   │   ├── README.md
│   │   ├── research.json
│   │   ├── schemas
│   │   │   ├── parameter.schema.json
│   │   │   ├── prompt-template.schema.json
│   │   │   ├── README.md
│   │   │   └── variant.schema.json
│   │   ├── update-subtask.json
│   │   ├── update-task.json
│   │   └── update-tasks.json
│   ├── provider-registry
│   │   └── index.js
│   ├── task-master.js
│   ├── ui
│   │   ├── confirm.js
│   │   ├── indicators.js
│   │   └── parse-prd.js
│   └── utils
│       ├── asset-resolver.js
│       ├── create-mcp-config.js
│       ├── format.js
│       ├── getVersion.js
│       ├── logger-utils.js
│       ├── manage-gitignore.js
│       ├── path-utils.js
│       ├── profiles.js
│       ├── rule-transformer.js
│       ├── stream-parser.js
│       └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│   ├── e2e
│   │   ├── e2e_helpers.sh
│   │   ├── parse_llm_output.cjs
│   │   ├── run_e2e.sh
│   │   ├── run_fallback_verification.sh
│   │   └── test_llm_analysis.sh
│   ├── fixture
│   │   └── test-tasks.json
│   ├── fixtures
│   │   ├── .taskmasterconfig
│   │   ├── sample-claude-response.js
│   │   ├── sample-prd.txt
│   │   └── sample-tasks.js
│   ├── integration
│   │   ├── claude-code-optional.test.js
│   │   ├── cli
│   │   │   ├── commands.test.js
│   │   │   ├── complex-cross-tag-scenarios.test.js
│   │   │   └── move-cross-tag.test.js
│   │   ├── manage-gitignore.test.js
│   │   ├── mcp-server
│   │   │   └── direct-functions.test.js
│   │   ├── move-task-cross-tag.integration.test.js
│   │   ├── move-task-simple.integration.test.js
│   │   └── profiles
│   │       ├── amp-init-functionality.test.js
│   │       ├── claude-init-functionality.test.js
│   │       ├── cline-init-functionality.test.js
│   │       ├── codex-init-functionality.test.js
│   │       ├── cursor-init-functionality.test.js
│   │       ├── gemini-init-functionality.test.js
│   │       ├── opencode-init-functionality.test.js
│   │       ├── roo-files-inclusion.test.js
│   │       ├── roo-init-functionality.test.js
│   │       ├── rules-files-inclusion.test.js
│   │       ├── trae-init-functionality.test.js
│   │       ├── vscode-init-functionality.test.js
│   │       └── windsurf-init-functionality.test.js
│   ├── manual
│   │   ├── progress
│   │   │   ├── parse-prd-analysis.js
│   │   │   ├── test-parse-prd.js
│   │   │   └── TESTING_GUIDE.md
│   │   └── prompts
│   │       ├── prompt-test.js
│   │       └── README.md
│   ├── README.md
│   ├── setup.js
│   └── unit
│       ├── ai-providers
│       │   ├── claude-code.test.js
│       │   ├── custom-sdk
│       │   │   └── claude-code
│       │   │       └── language-model.test.js
│       │   ├── gemini-cli.test.js
│       │   ├── mcp-components.test.js
│       │   └── openai.test.js
│       ├── ai-services-unified.test.js
│       ├── commands.test.js
│       ├── config-manager.test.js
│       ├── config-manager.test.mjs
│       ├── dependency-manager.test.js
│       ├── init.test.js
│       ├── initialize-project.test.js
│       ├── kebab-case-validation.test.js
│       ├── manage-gitignore.test.js
│       ├── mcp
│       │   └── tools
│       │       ├── __mocks__
│       │       │   └── move-task.js
│       │       ├── add-task.test.js
│       │       ├── analyze-complexity.test.js
│       │       ├── expand-all.test.js
│       │       ├── get-tasks.test.js
│       │       ├── initialize-project.test.js
│       │       ├── move-task-cross-tag-options.test.js
│       │       ├── move-task-cross-tag.test.js
│       │       └── remove-task.test.js
│       ├── mcp-providers
│       │   ├── mcp-components.test.js
│       │   └── mcp-provider.test.js
│       ├── parse-prd.test.js
│       ├── profiles
│       │   ├── amp-integration.test.js
│       │   ├── claude-integration.test.js
│       │   ├── cline-integration.test.js
│       │   ├── codex-integration.test.js
│       │   ├── cursor-integration.test.js
│       │   ├── gemini-integration.test.js
│       │   ├── kilo-integration.test.js
│       │   ├── kiro-integration.test.js
│       │   ├── mcp-config-validation.test.js
│       │   ├── opencode-integration.test.js
│       │   ├── profile-safety-check.test.js
│       │   ├── roo-integration.test.js
│       │   ├── rule-transformer-cline.test.js
│       │   ├── rule-transformer-cursor.test.js
│       │   ├── rule-transformer-gemini.test.js
│       │   ├── rule-transformer-kilo.test.js
│       │   ├── rule-transformer-kiro.test.js
│       │   ├── rule-transformer-opencode.test.js
│       │   ├── rule-transformer-roo.test.js
│       │   ├── rule-transformer-trae.test.js
│       │   ├── rule-transformer-vscode.test.js
│       │   ├── rule-transformer-windsurf.test.js
│       │   ├── rule-transformer-zed.test.js
│       │   ├── rule-transformer.test.js
│       │   ├── selective-profile-removal.test.js
│       │   ├── subdirectory-support.test.js
│       │   ├── trae-integration.test.js
│       │   ├── vscode-integration.test.js
│       │   ├── windsurf-integration.test.js
│       │   └── zed-integration.test.js
│       ├── progress
│       │   └── base-progress-tracker.test.js
│       ├── prompt-manager.test.js
│       ├── prompts
│       │   └── expand-task-prompt.test.js
│       ├── providers
│       │   └── provider-registry.test.js
│       ├── scripts
│       │   └── modules
│       │       ├── commands
│       │       │   ├── move-cross-tag.test.js
│       │       │   └── README.md
│       │       ├── dependency-manager
│       │       │   ├── circular-dependencies.test.js
│       │       │   ├── cross-tag-dependencies.test.js
│       │       │   └── fix-dependencies-command.test.js
│       │       ├── task-manager
│       │       │   ├── add-subtask.test.js
│       │       │   ├── add-task.test.js
│       │       │   ├── analyze-task-complexity.test.js
│       │       │   ├── clear-subtasks.test.js
│       │       │   ├── complexity-report-tag-isolation.test.js
│       │       │   ├── expand-all-tasks.test.js
│       │       │   ├── expand-task.test.js
│       │       │   ├── find-next-task.test.js
│       │       │   ├── generate-task-files.test.js
│       │       │   ├── list-tasks.test.js
│       │       │   ├── move-task-cross-tag.test.js
│       │       │   ├── move-task.test.js
│       │       │   ├── parse-prd.test.js
│       │       │   ├── remove-subtask.test.js
│       │       │   ├── remove-task.test.js
│       │       │   ├── research.test.js
│       │       │   ├── scope-adjustment.test.js
│       │       │   ├── set-task-status.test.js
│       │       │   ├── setup.js
│       │       │   ├── update-single-task-status.test.js
│       │       │   ├── update-subtask-by-id.test.js
│       │       │   ├── update-task-by-id.test.js
│       │       │   └── update-tasks.test.js
│       │       ├── ui
│       │       │   └── cross-tag-error-display.test.js
│       │       └── utils-tag-aware-paths.test.js
│       ├── task-finder.test.js
│       ├── task-manager
│       │   ├── clear-subtasks.test.js
│       │   ├── move-task.test.js
│       │   ├── tag-boundary.test.js
│       │   └── tag-management.test.js
│       ├── task-master.test.js
│       ├── ui
│       │   └── indicators.test.js
│       ├── ui.test.js
│       ├── utils-strip-ansi.test.js
│       └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
└── turbo.json
```

# Files

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

--------------------------------------------------------------------------------
/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 | 
```
Page 43/52FirstPrevNextLast