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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/workflow/orchestrators/workflow-orchestrator.ts:
--------------------------------------------------------------------------------

```typescript
  1 | import type { TestResultValidator } from '../services/test-result-validator.js';
  2 | import type {
  3 | 	StateTransition,
  4 | 	SubtaskInfo,
  5 | 	TDDPhase,
  6 | 	WorkflowContext,
  7 | 	WorkflowError,
  8 | 	WorkflowEvent,
  9 | 	WorkflowEventData,
 10 | 	WorkflowEventListener,
 11 | 	WorkflowEventType,
 12 | 	WorkflowPhase,
 13 | 	WorkflowState
 14 | } from '../types.js';
 15 | 
 16 | /**
 17 |  * Lightweight state machine for TDD workflow orchestration
 18 |  */
 19 | export class WorkflowOrchestrator {
 20 | 	private currentPhase: WorkflowPhase;
 21 | 	private context: WorkflowContext;
 22 | 	private readonly transitions: StateTransition[];
 23 | 	private readonly eventListeners: Map<
 24 | 		WorkflowEventType,
 25 | 		Set<WorkflowEventListener>
 26 | 	>;
 27 | 	private persistCallback?: (state: WorkflowState) => void | Promise<void>;
 28 | 	private autoPersistEnabled: boolean = false;
 29 | 	private readonly phaseGuards: Map<
 30 | 		WorkflowPhase,
 31 | 		(context: WorkflowContext) => boolean
 32 | 	>;
 33 | 	private aborted: boolean = false;
 34 | 	private testResultValidator?: TestResultValidator;
 35 | 	private gitOperationHook?: (operation: string, data?: unknown) => void;
 36 | 	private executeHook?: (command: string, context: WorkflowContext) => void;
 37 | 
 38 | 	constructor(initialContext: WorkflowContext) {
 39 | 		this.currentPhase = 'PREFLIGHT';
 40 | 		this.context = { ...initialContext };
 41 | 		this.transitions = this.defineTransitions();
 42 | 		this.eventListeners = new Map();
 43 | 		this.phaseGuards = new Map();
 44 | 	}
 45 | 
 46 | 	/**
 47 | 	 * Define valid state transitions
 48 | 	 */
 49 | 	private defineTransitions(): StateTransition[] {
 50 | 		return [
 51 | 			{
 52 | 				from: 'PREFLIGHT',
 53 | 				to: 'BRANCH_SETUP',
 54 | 				event: 'PREFLIGHT_COMPLETE'
 55 | 			},
 56 | 			{
 57 | 				from: 'BRANCH_SETUP',
 58 | 				to: 'SUBTASK_LOOP',
 59 | 				event: 'BRANCH_CREATED'
 60 | 			},
 61 | 			{
 62 | 				from: 'SUBTASK_LOOP',
 63 | 				to: 'FINALIZE',
 64 | 				event: 'ALL_SUBTASKS_COMPLETE'
 65 | 			},
 66 | 			{
 67 | 				from: 'FINALIZE',
 68 | 				to: 'COMPLETE',
 69 | 				event: 'FINALIZE_COMPLETE'
 70 | 			}
 71 | 		];
 72 | 	}
 73 | 
 74 | 	/**
 75 | 	 * Get current workflow phase
 76 | 	 */
 77 | 	getCurrentPhase(): WorkflowPhase {
 78 | 		return this.currentPhase;
 79 | 	}
 80 | 
 81 | 	/**
 82 | 	 * Get current TDD phase (only valid in SUBTASK_LOOP)
 83 | 	 */
 84 | 	getCurrentTDDPhase(): TDDPhase | undefined {
 85 | 		if (this.currentPhase === 'SUBTASK_LOOP') {
 86 | 			return this.context.currentTDDPhase || 'RED';
 87 | 		}
 88 | 		return undefined;
 89 | 	}
 90 | 
 91 | 	/**
 92 | 	 * Get workflow context
 93 | 	 */
 94 | 	getContext(): WorkflowContext {
 95 | 		return { ...this.context };
 96 | 	}
 97 | 
 98 | 	/**
 99 | 	 * Transition to next state based on event
100 | 	 */
101 | 	async transition(event: WorkflowEvent): Promise<void> {
102 | 		// Check if workflow is aborted
103 | 		if (this.aborted && event.type !== 'ABORT') {
104 | 			throw new Error('Workflow has been aborted');
105 | 		}
106 | 
107 | 		// Handle special events that work across all phases
108 | 		if (event.type === 'ERROR') {
109 | 			this.handleError(event.error);
110 | 			await this.triggerAutoPersist();
111 | 			return;
112 | 		}
113 | 
114 | 		if (event.type === 'ABORT') {
115 | 			this.aborted = true;
116 | 			await this.triggerAutoPersist();
117 | 			return;
118 | 		}
119 | 
120 | 		if (event.type === 'RETRY') {
121 | 			this.handleRetry();
122 | 			await this.triggerAutoPersist();
123 | 			return;
124 | 		}
125 | 
126 | 		// Handle TDD phase transitions within SUBTASK_LOOP
127 | 		if (this.currentPhase === 'SUBTASK_LOOP') {
128 | 			await this.handleTDDPhaseTransition(event);
129 | 			await this.triggerAutoPersist();
130 | 			return;
131 | 		}
132 | 
133 | 		// Handle main workflow phase transitions
134 | 		const validTransition = this.transitions.find(
135 | 			(t) => t.from === this.currentPhase && t.event === event.type
136 | 		);
137 | 
138 | 		if (!validTransition) {
139 | 			throw new Error(
140 | 				`Invalid transition: ${event.type} from ${this.currentPhase}`
141 | 			);
142 | 		}
143 | 
144 | 		// Execute transition
145 | 		this.executeTransition(validTransition, event);
146 | 		await this.triggerAutoPersist();
147 | 	}
148 | 
149 | 	/**
150 | 	 * Handle TDD phase transitions (RED -> GREEN -> COMMIT)
151 | 	 */
152 | 	private async handleTDDPhaseTransition(event: WorkflowEvent): Promise<void> {
153 | 		const currentTDD = this.context.currentTDDPhase || 'RED';
154 | 
155 | 		switch (event.type) {
156 | 			case 'RED_PHASE_COMPLETE':
157 | 				if (currentTDD !== 'RED') {
158 | 					throw new Error(
159 | 						'Invalid transition: RED_PHASE_COMPLETE from non-RED phase'
160 | 					);
161 | 				}
162 | 
163 | 				// Validate test results are provided
164 | 				if (!event.testResults) {
165 | 					throw new Error('Test results required for RED phase transition');
166 | 				}
167 | 
168 | 				// Store test results in context
169 | 				this.context.lastTestResults = event.testResults;
170 | 
171 | 				// Special case: All tests passing in RED phase means feature already implemented
172 | 				if (event.testResults.failed === 0) {
173 | 					this.emit('tdd:red:completed');
174 | 					this.emit('tdd:feature-already-implemented', {
175 | 						subtaskId: this.getCurrentSubtaskId(),
176 | 						testResults: event.testResults
177 | 					});
178 | 
179 | 					// Mark subtask as complete and move to next one
180 | 					const subtask =
181 | 						this.context.subtasks[this.context.currentSubtaskIndex];
182 | 					if (subtask) {
183 | 						subtask.status = 'completed';
184 | 					}
185 | 
186 | 					this.emit('subtask:completed');
187 | 					this.context.currentSubtaskIndex++;
188 | 
189 | 					// Emit progress update
190 | 					const progress = this.getProgress();
191 | 					this.emit('progress:updated', {
192 | 						completed: progress.completed,
193 | 						total: progress.total,
194 | 						percentage: progress.percentage
195 | 					});
196 | 
197 | 					// Start next subtask or complete workflow
198 | 					if (this.context.currentSubtaskIndex < this.context.subtasks.length) {
199 | 						this.context.currentTDDPhase = 'RED';
200 | 						this.emit('tdd:red:started');
201 | 						this.emit('subtask:started');
202 | 					} else {
203 | 						// All subtasks complete, transition to FINALIZE
204 | 						await this.transition({ type: 'ALL_SUBTASKS_COMPLETE' });
205 | 					}
206 | 					break;
207 | 				}
208 | 
209 | 				// Normal RED phase: has failing tests, proceed to GREEN
210 | 				this.emit('tdd:red:completed');
211 | 				this.context.currentTDDPhase = 'GREEN';
212 | 				this.emit('tdd:green:started');
213 | 				break;
214 | 
215 | 			case 'GREEN_PHASE_COMPLETE':
216 | 				if (currentTDD !== 'GREEN') {
217 | 					throw new Error(
218 | 						'Invalid transition: GREEN_PHASE_COMPLETE from non-GREEN phase'
219 | 					);
220 | 				}
221 | 
222 | 				// Validate test results are provided
223 | 				if (!event.testResults) {
224 | 					throw new Error('Test results required for GREEN phase transition');
225 | 				}
226 | 
227 | 				// Validate GREEN phase has no failures
228 | 				if (event.testResults.failed !== 0) {
229 | 					throw new Error('GREEN phase must have zero failures');
230 | 				}
231 | 
232 | 				// Store test results in context
233 | 				this.context.lastTestResults = event.testResults;
234 | 
235 | 				this.emit('tdd:green:completed');
236 | 				this.context.currentTDDPhase = 'COMMIT';
237 | 				this.emit('tdd:commit:started');
238 | 				break;
239 | 
240 | 			case 'COMMIT_COMPLETE':
241 | 				if (currentTDD !== 'COMMIT') {
242 | 					throw new Error(
243 | 						'Invalid transition: COMMIT_COMPLETE from non-COMMIT phase'
244 | 					);
245 | 				}
246 | 				this.emit('tdd:commit:completed');
247 | 				// Mark current subtask as completed
248 | 				const currentSubtask =
249 | 					this.context.subtasks[this.context.currentSubtaskIndex];
250 | 				if (currentSubtask) {
251 | 					currentSubtask.status = 'completed';
252 | 				}
253 | 				break;
254 | 
255 | 			case 'SUBTASK_COMPLETE':
256 | 				this.emit('subtask:completed');
257 | 				// Move to next subtask
258 | 				this.context.currentSubtaskIndex++;
259 | 
260 | 				// Emit progress update
261 | 				const progress = this.getProgress();
262 | 				this.emit('progress:updated', {
263 | 					completed: progress.completed,
264 | 					total: progress.total,
265 | 					percentage: progress.percentage
266 | 				});
267 | 
268 | 				if (this.context.currentSubtaskIndex < this.context.subtasks.length) {
269 | 					// Start next subtask with RED phase
270 | 					this.context.currentTDDPhase = 'RED';
271 | 					this.emit('tdd:red:started');
272 | 					this.emit('subtask:started');
273 | 				} else {
274 | 					// All subtasks complete, transition to FINALIZE
275 | 					await this.transition({ type: 'ALL_SUBTASKS_COMPLETE' });
276 | 				}
277 | 				break;
278 | 
279 | 			case 'ALL_SUBTASKS_COMPLETE':
280 | 				// Transition to FINALIZE phase
281 | 				this.emit('phase:exited');
282 | 				this.currentPhase = 'FINALIZE';
283 | 				this.context.currentTDDPhase = undefined;
284 | 				this.emit('phase:entered');
285 | 				// Note: Don't auto-transition to COMPLETE - requires explicit finalize call
286 | 				break;
287 | 
288 | 			default:
289 | 				throw new Error(`Invalid transition: ${event.type} in SUBTASK_LOOP`);
290 | 		}
291 | 	}
292 | 
293 | 	/**
294 | 	 * Execute a state transition
295 | 	 */
296 | 	private executeTransition(
297 | 		transition: StateTransition,
298 | 		event: WorkflowEvent
299 | 	): void {
300 | 		// Check guard condition if present
301 | 		if (transition.guard && !transition.guard(this.context)) {
302 | 			throw new Error(
303 | 				`Guard condition failed for transition to ${transition.to}`
304 | 			);
305 | 		}
306 | 
307 | 		// Check phase-specific guard if present
308 | 		const phaseGuard = this.phaseGuards.get(transition.to);
309 | 		if (phaseGuard && !phaseGuard(this.context)) {
310 | 			throw new Error('Guard condition failed');
311 | 		}
312 | 
313 | 		// Emit phase exit event
314 | 		this.emit('phase:exited');
315 | 
316 | 		// Update context based on event
317 | 		this.updateContext(event);
318 | 
319 | 		// Transition to new phase
320 | 		this.currentPhase = transition.to;
321 | 
322 | 		// Emit phase entry event
323 | 		this.emit('phase:entered');
324 | 
325 | 		// Initialize TDD phase if entering SUBTASK_LOOP
326 | 		if (this.currentPhase === 'SUBTASK_LOOP') {
327 | 			this.context.currentTDDPhase = 'RED';
328 | 			this.emit('tdd:red:started');
329 | 			this.emit('subtask:started');
330 | 		}
331 | 	}
332 | 
333 | 	/**
334 | 	 * Update context based on event
335 | 	 */
336 | 	private updateContext(event: WorkflowEvent): void {
337 | 		switch (event.type) {
338 | 			case 'BRANCH_CREATED':
339 | 				this.context.branchName = event.branchName;
340 | 				this.emit('git:branch:created', { branchName: event.branchName });
341 | 
342 | 				// Trigger git operation hook
343 | 				if (this.gitOperationHook) {
344 | 					this.gitOperationHook('branch:created', {
345 | 						branchName: event.branchName
346 | 					});
347 | 				}
348 | 				break;
349 | 
350 | 			case 'ERROR':
351 | 				this.context.errors.push(event.error);
352 | 				this.emit('error:occurred', { error: event.error });
353 | 				break;
354 | 
355 | 			// Add more context updates as needed
356 | 		}
357 | 	}
358 | 
359 | 	/**
360 | 	 * Get current state for serialization
361 | 	 */
362 | 	getState(): WorkflowState {
363 | 		return {
364 | 			phase: this.currentPhase,
365 | 			context: { ...this.context }
366 | 		};
367 | 	}
368 | 
369 | 	/**
370 | 	 * Restore state from checkpoint
371 | 	 */
372 | 	restoreState(state: WorkflowState): void {
373 | 		this.currentPhase = state.phase;
374 | 		this.context = { ...state.context };
375 | 
376 | 		// Emit workflow:resumed event
377 | 		this.emit('workflow:resumed', {
378 | 			phase: this.currentPhase,
379 | 			progress: this.getProgress()
380 | 		});
381 | 	}
382 | 
383 | 	/**
384 | 	 * Add event listener
385 | 	 */
386 | 	on(eventType: WorkflowEventType, listener: WorkflowEventListener): void {
387 | 		if (!this.eventListeners.has(eventType)) {
388 | 			this.eventListeners.set(eventType, new Set());
389 | 		}
390 | 		this.eventListeners.get(eventType)!.add(listener);
391 | 	}
392 | 
393 | 	/**
394 | 	 * Remove event listener
395 | 	 */
396 | 	off(eventType: WorkflowEventType, listener: WorkflowEventListener): void {
397 | 		const listeners = this.eventListeners.get(eventType);
398 | 		if (listeners) {
399 | 			listeners.delete(listener);
400 | 		}
401 | 	}
402 | 
403 | 	/**
404 | 	 * Emit workflow event
405 | 	 */
406 | 	private emit(
407 | 		eventType: WorkflowEventType,
408 | 		data?: Record<string, unknown>
409 | 	): void {
410 | 		const eventData: WorkflowEventData = {
411 | 			type: eventType,
412 | 			timestamp: new Date(),
413 | 			phase: this.currentPhase,
414 | 			tddPhase: this.context.currentTDDPhase,
415 | 			subtaskId: this.getCurrentSubtaskId(),
416 | 			data: {
417 | 				...data,
418 | 				adapters: {
419 | 					testValidator: !!this.testResultValidator,
420 | 					gitHook: !!this.gitOperationHook,
421 | 					executeHook: !!this.executeHook
422 | 				}
423 | 			}
424 | 		};
425 | 
426 | 		const listeners = this.eventListeners.get(eventType);
427 | 		if (listeners) {
428 | 			listeners.forEach((listener) => listener(eventData));
429 | 		}
430 | 	}
431 | 
432 | 	/**
433 | 	 * Get current subtask ID
434 | 	 */
435 | 	private getCurrentSubtaskId(): string | undefined {
436 | 		const currentSubtask =
437 | 			this.context.subtasks[this.context.currentSubtaskIndex];
438 | 		return currentSubtask?.id;
439 | 	}
440 | 
441 | 	/**
442 | 	 * Register callback for state persistence
443 | 	 */
444 | 	onStatePersist(
445 | 		callback: (state: WorkflowState) => void | Promise<void>
446 | 	): void {
447 | 		this.persistCallback = callback;
448 | 	}
449 | 
450 | 	/**
451 | 	 * Enable auto-persistence after each transition
452 | 	 */
453 | 	enableAutoPersist(
454 | 		callback: (state: WorkflowState) => void | Promise<void>
455 | 	): void {
456 | 		this.persistCallback = callback;
457 | 		this.autoPersistEnabled = true;
458 | 	}
459 | 
460 | 	/**
461 | 	 * Disable auto-persistence
462 | 	 */
463 | 	disableAutoPersist(): void {
464 | 		this.autoPersistEnabled = false;
465 | 	}
466 | 
467 | 	/**
468 | 	 * Manually persist current state
469 | 	 */
470 | 	async persistState(): Promise<void> {
471 | 		if (this.persistCallback) {
472 | 			await this.persistCallback(this.getState());
473 | 		}
474 | 		this.emit('state:persisted');
475 | 	}
476 | 
477 | 	/**
478 | 	 * Trigger auto-persistence if enabled
479 | 	 */
480 | 	private async triggerAutoPersist(): Promise<void> {
481 | 		if (this.autoPersistEnabled && this.persistCallback) {
482 | 			await this.persistCallback(this.getState());
483 | 		}
484 | 	}
485 | 
486 | 	/**
487 | 	 * Add a guard condition for a specific phase
488 | 	 */
489 | 	addGuard(
490 | 		phase: WorkflowPhase,
491 | 		guard: (context: WorkflowContext) => boolean
492 | 	): void {
493 | 		this.phaseGuards.set(phase, guard);
494 | 	}
495 | 
496 | 	/**
497 | 	 * Remove a guard condition for a specific phase
498 | 	 */
499 | 	removeGuard(phase: WorkflowPhase): void {
500 | 		this.phaseGuards.delete(phase);
501 | 	}
502 | 
503 | 	/**
504 | 	 * Get current subtask being worked on
505 | 	 */
506 | 	getCurrentSubtask(): SubtaskInfo | undefined {
507 | 		return this.context.subtasks[this.context.currentSubtaskIndex];
508 | 	}
509 | 
510 | 	/**
511 | 	 * Get workflow progress information
512 | 	 */
513 | 	getProgress(): {
514 | 		completed: number;
515 | 		total: number;
516 | 		current: number;
517 | 		percentage: number;
518 | 	} {
519 | 		const completed = this.context.subtasks.filter(
520 | 			(st) => st.status === 'completed'
521 | 		).length;
522 | 		const total = this.context.subtasks.length;
523 | 		const current = this.context.currentSubtaskIndex + 1;
524 | 		const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
525 | 
526 | 		return { completed, total, current, percentage };
527 | 	}
528 | 
529 | 	/**
530 | 	 * Check if can proceed to next subtask or phase
531 | 	 */
532 | 	canProceed(): boolean {
533 | 		if (this.currentPhase !== 'SUBTASK_LOOP') {
534 | 			return false;
535 | 		}
536 | 
537 | 		const currentSubtask = this.getCurrentSubtask();
538 | 
539 | 		// Can proceed if current subtask is completed (after COMMIT phase)
540 | 		return currentSubtask?.status === 'completed';
541 | 	}
542 | 
543 | 	/**
544 | 	 * Increment attempts for current subtask
545 | 	 */
546 | 	incrementAttempts(): void {
547 | 		const currentSubtask = this.getCurrentSubtask();
548 | 		if (currentSubtask) {
549 | 			currentSubtask.attempts++;
550 | 		}
551 | 	}
552 | 
553 | 	/**
554 | 	 * Check if current subtask has exceeded max attempts
555 | 	 */
556 | 	hasExceededMaxAttempts(): boolean {
557 | 		const currentSubtask = this.getCurrentSubtask();
558 | 		if (!currentSubtask || !currentSubtask.maxAttempts) {
559 | 			return false;
560 | 		}
561 | 
562 | 		return currentSubtask.attempts > currentSubtask.maxAttempts;
563 | 	}
564 | 
565 | 	/**
566 | 	 * Handle error event
567 | 	 */
568 | 	private handleError(error: WorkflowError): void {
569 | 		this.context.errors.push(error);
570 | 		this.emit('error:occurred', { error });
571 | 	}
572 | 
573 | 	/**
574 | 	 * Handle retry event
575 | 	 */
576 | 	private handleRetry(): void {
577 | 		if (this.currentPhase === 'SUBTASK_LOOP') {
578 | 			// Reset to RED phase to retry current subtask
579 | 			this.context.currentTDDPhase = 'RED';
580 | 			this.emit('tdd:red:started');
581 | 		}
582 | 	}
583 | 
584 | 	/**
585 | 	 * Retry current subtask (resets to RED phase)
586 | 	 */
587 | 	retryCurrentSubtask(): void {
588 | 		if (this.currentPhase === 'SUBTASK_LOOP') {
589 | 			this.context.currentTDDPhase = 'RED';
590 | 			this.emit('tdd:red:started');
591 | 		}
592 | 	}
593 | 
594 | 	/**
595 | 	 * Handle max attempts exceeded for current subtask
596 | 	 */
597 | 	handleMaxAttemptsExceeded(): void {
598 | 		const currentSubtask = this.getCurrentSubtask();
599 | 		if (currentSubtask) {
600 | 			currentSubtask.status = 'failed';
601 | 			this.emit('subtask:failed', {
602 | 				subtaskId: currentSubtask.id,
603 | 				attempts: currentSubtask.attempts,
604 | 				maxAttempts: currentSubtask.maxAttempts
605 | 			});
606 | 		}
607 | 	}
608 | 
609 | 	/**
610 | 	 * Check if workflow has been aborted
611 | 	 */
612 | 	isAborted(): boolean {
613 | 		return this.aborted;
614 | 	}
615 | 
616 | 	/**
617 | 	 * Validate if a state can be resumed from
618 | 	 */
619 | 	canResumeFromState(state: WorkflowState): boolean {
620 | 		// Validate phase is valid
621 | 		const validPhases: WorkflowPhase[] = [
622 | 			'PREFLIGHT',
623 | 			'BRANCH_SETUP',
624 | 			'SUBTASK_LOOP',
625 | 			'FINALIZE',
626 | 			'COMPLETE'
627 | 		];
628 | 
629 | 		if (!validPhases.includes(state.phase)) {
630 | 			return false;
631 | 		}
632 | 
633 | 		// Validate context structure
634 | 		if (!state.context || typeof state.context !== 'object') {
635 | 			return false;
636 | 		}
637 | 
638 | 		// Validate required context fields
639 | 		if (!state.context.taskId || !Array.isArray(state.context.subtasks)) {
640 | 			return false;
641 | 		}
642 | 
643 | 		if (typeof state.context.currentSubtaskIndex !== 'number') {
644 | 			return false;
645 | 		}
646 | 
647 | 		if (!Array.isArray(state.context.errors)) {
648 | 			return false;
649 | 		}
650 | 
651 | 		// All validations passed
652 | 		return true;
653 | 	}
654 | 
655 | 	/**
656 | 	 * Set TestResultValidator adapter
657 | 	 */
658 | 	setTestResultValidator(validator: TestResultValidator): void {
659 | 		this.testResultValidator = validator;
660 | 		this.emit('adapter:configured', { adapterType: 'test-validator' });
661 | 	}
662 | 
663 | 	/**
664 | 	 * Check if TestResultValidator is configured
665 | 	 */
666 | 	hasTestResultValidator(): boolean {
667 | 		return !!this.testResultValidator;
668 | 	}
669 | 
670 | 	/**
671 | 	 * Remove TestResultValidator adapter
672 | 	 */
673 | 	removeTestResultValidator(): void {
674 | 		this.testResultValidator = undefined;
675 | 	}
676 | 
677 | 	/**
678 | 	 * Register git operation hook
679 | 	 */
680 | 	onGitOperation(hook: (operation: string, data?: unknown) => void): void {
681 | 		this.gitOperationHook = hook;
682 | 	}
683 | 
684 | 	/**
685 | 	 * Register execute command hook
686 | 	 */
687 | 	onExecute(hook: (command: string, context: WorkflowContext) => void): void {
688 | 		this.executeHook = hook;
689 | 	}
690 | 
691 | 	/**
692 | 	 * Execute a command (triggers execute hook)
693 | 	 */
694 | 	executeCommand(command: string): void {
695 | 		if (this.executeHook) {
696 | 			this.executeHook(command, this.context);
697 | 		}
698 | 	}
699 | }
700 | 
```

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

```javascript
  1 | import { jest } from '@jest/globals';
  2 | 
  3 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
  4 | 	findProjectRoot: jest.fn(() => '/test/project/root'),
  5 | 	log: jest.fn(),
  6 | 	readJSON: jest.fn(),
  7 | 	flattenTasksWithSubtasks: jest.fn(() => []),
  8 | 	isEmpty: jest.fn(() => false)
  9 | }));
 10 | 
 11 | // Mock UI-affecting external libs to minimal no-op implementations
 12 | jest.unstable_mockModule('chalk', () => ({
 13 | 	default: {
 14 | 		white: Object.assign(
 15 | 			jest.fn((text) => text),
 16 | 			{
 17 | 				bold: jest.fn((text) => text)
 18 | 			}
 19 | 		),
 20 | 		cyan: Object.assign(
 21 | 			jest.fn((text) => text),
 22 | 			{
 23 | 				bold: jest.fn((text) => text)
 24 | 			}
 25 | 		),
 26 | 		green: Object.assign(
 27 | 			jest.fn((text) => text),
 28 | 			{
 29 | 				bold: jest.fn((text) => text)
 30 | 			}
 31 | 		),
 32 | 		yellow: jest.fn((text) => text),
 33 | 		red: jest.fn((text) => text),
 34 | 		gray: jest.fn((text) => text),
 35 | 		blue: Object.assign(
 36 | 			jest.fn((text) => text),
 37 | 			{
 38 | 				bold: jest.fn((text) => text)
 39 | 			}
 40 | 		),
 41 | 		bold: jest.fn((text) => text)
 42 | 	}
 43 | }));
 44 | 
 45 | jest.unstable_mockModule('boxen', () => ({ default: (text) => text }));
 46 | 
 47 | jest.unstable_mockModule('inquirer', () => ({
 48 | 	default: { prompt: jest.fn() }
 49 | }));
 50 | 
 51 | jest.unstable_mockModule('cli-highlight', () => ({
 52 | 	highlight: (code) => code
 53 | }));
 54 | 
 55 | jest.unstable_mockModule('cli-table3', () => ({
 56 | 	default: jest.fn().mockImplementation(() => ({
 57 | 		push: jest.fn(),
 58 | 		toString: jest.fn(() => '')
 59 | 	}))
 60 | }));
 61 | 
 62 | jest.unstable_mockModule(
 63 | 	'../../../../../scripts/modules/utils/contextGatherer.js',
 64 | 	() => ({
 65 | 		ContextGatherer: jest.fn().mockImplementation(() => ({
 66 | 			gather: jest.fn().mockResolvedValue({
 67 | 				context: 'Gathered context',
 68 | 				tokenBreakdown: { total: 500 }
 69 | 			}),
 70 | 			countTokens: jest.fn(() => 100)
 71 | 		}))
 72 | 	})
 73 | );
 74 | 
 75 | jest.unstable_mockModule(
 76 | 	'../../../../../scripts/modules/utils/fuzzyTaskSearch.js',
 77 | 	() => ({
 78 | 		FuzzyTaskSearch: jest.fn().mockImplementation(() => ({
 79 | 			findRelevantTasks: jest.fn(() => []),
 80 | 			getTaskIds: jest.fn(() => [])
 81 | 		}))
 82 | 	})
 83 | );
 84 | 
 85 | jest.unstable_mockModule(
 86 | 	'../../../../../scripts/modules/ai-services-unified.js',
 87 | 	() => ({
 88 | 		generateTextService: jest.fn().mockResolvedValue({
 89 | 			mainResult:
 90 | 				'Test research result with ```javascript\nconsole.log("test");\n```',
 91 | 			telemetryData: {}
 92 | 		})
 93 | 	})
 94 | );
 95 | 
 96 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
 97 | 	displayAiUsageSummary: jest.fn(),
 98 | 	startLoadingIndicator: jest.fn(() => ({ stop: jest.fn() })),
 99 | 	stopLoadingIndicator: jest.fn()
100 | }));
101 | 
102 | jest.unstable_mockModule(
103 | 	'../../../../../scripts/modules/prompt-manager.js',
104 | 	() => ({
105 | 		getPromptManager: jest.fn().mockReturnValue({
106 | 			loadPrompt: jest.fn().mockResolvedValue({
107 | 				systemPrompt: 'System prompt',
108 | 				userPrompt: 'User prompt'
109 | 			})
110 | 		})
111 | 	})
112 | );
113 | 
114 | const { performResearch } = await import(
115 | 	'../../../../../scripts/modules/task-manager/research.js'
116 | );
117 | 
118 | // Import mocked modules for testing
119 | const utils = await import('../../../../../scripts/modules/utils.js');
120 | const { ContextGatherer } = await import(
121 | 	'../../../../../scripts/modules/utils/contextGatherer.js'
122 | );
123 | const { FuzzyTaskSearch } = await import(
124 | 	'../../../../../scripts/modules/utils/fuzzyTaskSearch.js'
125 | );
126 | const { generateTextService } = await import(
127 | 	'../../../../../scripts/modules/ai-services-unified.js'
128 | );
129 | 
130 | describe('performResearch project root validation', () => {
131 | 	it('throws error when project root cannot be determined', async () => {
132 | 		// Mock findProjectRoot to return null
133 | 		utils.findProjectRoot.mockReturnValueOnce(null);
134 | 
135 | 		await expect(
136 | 			performResearch('Test query', {}, {}, 'json', false)
137 | 		).rejects.toThrow('Could not determine project root directory');
138 | 	});
139 | });
140 | 
141 | describe('performResearch tag-aware functionality', () => {
142 | 	let mockContextGatherer;
143 | 	let mockFuzzySearch;
144 | 	let mockReadJSON;
145 | 	let mockFlattenTasks;
146 | 
147 | 	beforeEach(() => {
148 | 		// Reset all mocks
149 | 		jest.clearAllMocks();
150 | 
151 | 		// Set up default mocks
152 | 		utils.findProjectRoot.mockReturnValue('/test/project/root');
153 | 		utils.readJSON.mockResolvedValue({
154 | 			tasks: [
155 | 				{ id: 1, title: 'Task 1', description: 'Description 1' },
156 | 				{ id: 2, title: 'Task 2', description: 'Description 2' }
157 | 			]
158 | 		});
159 | 		utils.flattenTasksWithSubtasks.mockReturnValue([
160 | 			{ id: 1, title: 'Task 1', description: 'Description 1' },
161 | 			{ id: 2, title: 'Task 2', description: 'Description 2' }
162 | 		]);
163 | 
164 | 		// Set up ContextGatherer mock
165 | 		mockContextGatherer = {
166 | 			gather: jest.fn().mockResolvedValue({
167 | 				context: 'Gathered context',
168 | 				tokenBreakdown: { total: 500 }
169 | 			}),
170 | 			countTokens: jest.fn(() => 100)
171 | 		};
172 | 		ContextGatherer.mockImplementation(() => mockContextGatherer);
173 | 
174 | 		// Set up FuzzyTaskSearch mock
175 | 		mockFuzzySearch = {
176 | 			findRelevantTasks: jest.fn(() => [
177 | 				{ id: 1, title: 'Task 1', description: 'Description 1' }
178 | 			]),
179 | 			getTaskIds: jest.fn(() => ['1'])
180 | 		};
181 | 		FuzzyTaskSearch.mockImplementation(() => mockFuzzySearch);
182 | 
183 | 		// Store references for easier access
184 | 		mockReadJSON = utils.readJSON;
185 | 		mockFlattenTasks = utils.flattenTasksWithSubtasks;
186 | 	});
187 | 
188 | 	describe('tag parameter passing to ContextGatherer', () => {
189 | 		it('passes tag parameter to ContextGatherer constructor', async () => {
190 | 			const testTag = 'feature-branch';
191 | 
192 | 			await performResearch('Test query', { tag: testTag }, {}, 'json', false);
193 | 
194 | 			expect(ContextGatherer).toHaveBeenCalledWith(
195 | 				'/test/project/root',
196 | 				testTag
197 | 			);
198 | 		});
199 | 
200 | 		it('passes undefined tag when no tag is provided', async () => {
201 | 			await performResearch('Test query', {}, {}, 'json', false);
202 | 
203 | 			expect(ContextGatherer).toHaveBeenCalledWith(
204 | 				'/test/project/root',
205 | 				undefined
206 | 			);
207 | 		});
208 | 
209 | 		it('passes empty string tag when empty string is provided', async () => {
210 | 			await performResearch('Test query', { tag: '' }, {}, 'json', false);
211 | 
212 | 			expect(ContextGatherer).toHaveBeenCalledWith('/test/project/root', '');
213 | 		});
214 | 
215 | 		it('passes null tag when null is provided', async () => {
216 | 			await performResearch('Test query', { tag: null }, {}, 'json', false);
217 | 
218 | 			expect(ContextGatherer).toHaveBeenCalledWith('/test/project/root', null);
219 | 		});
220 | 	});
221 | 
222 | 	describe('tag-aware readJSON calls', () => {
223 | 		it('calls readJSON with correct tag parameter for task discovery', async () => {
224 | 			const testTag = 'development';
225 | 
226 | 			await performResearch('Test query', { tag: testTag }, {}, 'json', false);
227 | 
228 | 			expect(mockReadJSON).toHaveBeenCalledWith(
229 | 				expect.stringContaining('tasks.json'),
230 | 				'/test/project/root',
231 | 				testTag
232 | 			);
233 | 		});
234 | 
235 | 		it('calls readJSON with undefined tag when no tag provided', async () => {
236 | 			await performResearch('Test query', {}, {}, 'json', false);
237 | 
238 | 			expect(mockReadJSON).toHaveBeenCalledWith(
239 | 				expect.stringContaining('tasks.json'),
240 | 				'/test/project/root',
241 | 				undefined
242 | 			);
243 | 		});
244 | 
245 | 		it('calls readJSON with provided projectRoot and tag', async () => {
246 | 			const customProjectRoot = '/custom/project/root';
247 | 			const testTag = 'production';
248 | 
249 | 			await performResearch(
250 | 				'Test query',
251 | 				{
252 | 					projectRoot: customProjectRoot,
253 | 					tag: testTag
254 | 				},
255 | 				{},
256 | 				'json',
257 | 				false
258 | 			);
259 | 
260 | 			expect(mockReadJSON).toHaveBeenCalledWith(
261 | 				expect.stringContaining('tasks.json'),
262 | 				customProjectRoot,
263 | 				testTag
264 | 			);
265 | 		});
266 | 	});
267 | 
268 | 	describe('context gathering behavior for different tags', () => {
269 | 		it('calls contextGatherer.gather with correct parameters', async () => {
270 | 			const options = {
271 | 				taskIds: ['1', '2'],
272 | 				filePaths: ['src/file.js'],
273 | 				customContext: 'Custom context',
274 | 				includeProjectTree: true,
275 | 				tag: 'feature-branch'
276 | 			};
277 | 
278 | 			await performResearch('Test query', options, {}, 'json', false);
279 | 
280 | 			expect(mockContextGatherer.gather).toHaveBeenCalledWith({
281 | 				tasks: expect.arrayContaining(['1', '2']),
282 | 				files: ['src/file.js'],
283 | 				customContext: 'Custom context',
284 | 				includeProjectTree: true,
285 | 				format: 'research',
286 | 				includeTokenCounts: true
287 | 			});
288 | 		});
289 | 
290 | 		it('handles empty task discovery gracefully when readJSON fails', async () => {
291 | 			mockReadJSON.mockRejectedValueOnce(new Error('File not found'));
292 | 
293 | 			const result = await performResearch(
294 | 				'Test query',
295 | 				{ tag: 'test-tag' },
296 | 				{},
297 | 				'json',
298 | 				false
299 | 			);
300 | 
301 | 			// Should still succeed even if task discovery fails
302 | 			expect(result).toBeDefined();
303 | 			expect(mockContextGatherer.gather).toHaveBeenCalledWith({
304 | 				tasks: [],
305 | 				files: [],
306 | 				customContext: '',
307 | 				includeProjectTree: false,
308 | 				format: 'research',
309 | 				includeTokenCounts: true
310 | 			});
311 | 		});
312 | 
313 | 		it('combines provided taskIds with auto-discovered tasks', async () => {
314 | 			const providedTaskIds = ['3', '4'];
315 | 			const autoDiscoveredIds = ['1', '2'];
316 | 
317 | 			mockFuzzySearch.getTaskIds.mockReturnValue(autoDiscoveredIds);
318 | 
319 | 			await performResearch(
320 | 				'Test query',
321 | 				{
322 | 					taskIds: providedTaskIds,
323 | 					tag: 'feature-branch'
324 | 				},
325 | 				{},
326 | 				'json',
327 | 				false
328 | 			);
329 | 
330 | 			expect(mockContextGatherer.gather).toHaveBeenCalledWith({
331 | 				tasks: expect.arrayContaining([
332 | 					...providedTaskIds,
333 | 					...autoDiscoveredIds
334 | 				]),
335 | 				files: [],
336 | 				customContext: '',
337 | 				includeProjectTree: false,
338 | 				format: 'research',
339 | 				includeTokenCounts: true
340 | 			});
341 | 		});
342 | 
343 | 		it('removes duplicate tasks when auto-discovered tasks overlap with provided tasks', async () => {
344 | 			const providedTaskIds = ['1', '2'];
345 | 			const autoDiscoveredIds = ['2', '3']; // '2' is duplicate
346 | 
347 | 			mockFuzzySearch.getTaskIds.mockReturnValue(autoDiscoveredIds);
348 | 
349 | 			await performResearch(
350 | 				'Test query',
351 | 				{
352 | 					taskIds: providedTaskIds,
353 | 					tag: 'feature-branch'
354 | 				},
355 | 				{},
356 | 				'json',
357 | 				false
358 | 			);
359 | 
360 | 			expect(mockContextGatherer.gather).toHaveBeenCalledWith({
361 | 				tasks: ['1', '2', '3'], // Should include '3' but not duplicate '2'
362 | 				files: [],
363 | 				customContext: '',
364 | 				includeProjectTree: false,
365 | 				format: 'research',
366 | 				includeTokenCounts: true
367 | 			});
368 | 		});
369 | 	});
370 | 
371 | 	describe('tag-aware fuzzy search', () => {
372 | 		it('initializes FuzzyTaskSearch with flattened tasks from correct tag', async () => {
373 | 			const testTag = 'development';
374 | 			const mockFlattenedTasks = [
375 | 				{ id: 1, title: 'Dev Task 1' },
376 | 				{ id: 2, title: 'Dev Task 2' }
377 | 			];
378 | 
379 | 			mockFlattenTasks.mockReturnValue(mockFlattenedTasks);
380 | 
381 | 			await performResearch('Test query', { tag: testTag }, {}, 'json', false);
382 | 
383 | 			expect(mockFlattenTasks).toHaveBeenCalledWith(
384 | 				expect.arrayContaining([
385 | 					expect.objectContaining({ id: 1 }),
386 | 					expect.objectContaining({ id: 2 })
387 | 				])
388 | 			);
389 | 			expect(FuzzyTaskSearch).toHaveBeenCalledWith(
390 | 				mockFlattenedTasks,
391 | 				'research'
392 | 			);
393 | 		});
394 | 
395 | 		it('calls fuzzy search with correct parameters', async () => {
396 | 			const testQuery = 'authentication implementation';
397 | 
398 | 			await performResearch(
399 | 				testQuery,
400 | 				{ tag: 'feature-branch' },
401 | 				{},
402 | 				'json',
403 | 				false
404 | 			);
405 | 
406 | 			expect(mockFuzzySearch.findRelevantTasks).toHaveBeenCalledWith(
407 | 				testQuery,
408 | 				{
409 | 					maxResults: 8,
410 | 					includeRecent: true,
411 | 					includeCategoryMatches: true
412 | 				}
413 | 			);
414 | 		});
415 | 
416 | 		it('handles empty tasks data gracefully', async () => {
417 | 			mockReadJSON.mockResolvedValueOnce({ tasks: [] });
418 | 
419 | 			await performResearch(
420 | 				'Test query',
421 | 				{ tag: 'empty-tag' },
422 | 				{},
423 | 				'json',
424 | 				false
425 | 			);
426 | 
427 | 			// Should not call FuzzyTaskSearch when no tasks exist
428 | 			expect(FuzzyTaskSearch).not.toHaveBeenCalled();
429 | 			expect(mockContextGatherer.gather).toHaveBeenCalledWith({
430 | 				tasks: [],
431 | 				files: [],
432 | 				customContext: '',
433 | 				includeProjectTree: false,
434 | 				format: 'research',
435 | 				includeTokenCounts: true
436 | 			});
437 | 		});
438 | 
439 | 		it('handles null tasks data gracefully', async () => {
440 | 			mockReadJSON.mockResolvedValueOnce(null);
441 | 
442 | 			await performResearch(
443 | 				'Test query',
444 | 				{ tag: 'null-tag' },
445 | 				{},
446 | 				'json',
447 | 				false
448 | 			);
449 | 
450 | 			// Should not call FuzzyTaskSearch when data is null
451 | 			expect(FuzzyTaskSearch).not.toHaveBeenCalled();
452 | 		});
453 | 	});
454 | 
455 | 	describe('error handling for invalid tags', () => {
456 | 		it('continues execution when readJSON throws error for invalid tag', async () => {
457 | 			mockReadJSON.mockRejectedValueOnce(new Error('Tag not found'));
458 | 
459 | 			const result = await performResearch(
460 | 				'Test query',
461 | 				{ tag: 'invalid-tag' },
462 | 				{},
463 | 				'json',
464 | 				false
465 | 			);
466 | 
467 | 			// Should still succeed and return a result
468 | 			expect(result).toBeDefined();
469 | 			expect(mockContextGatherer.gather).toHaveBeenCalled();
470 | 		});
471 | 
472 | 		it('logs debug message when task discovery fails', async () => {
473 | 			const mockLog = {
474 | 				debug: jest.fn(),
475 | 				info: jest.fn(),
476 | 				warn: jest.fn(),
477 | 				error: jest.fn(),
478 | 				success: jest.fn()
479 | 			};
480 | 
481 | 			mockReadJSON.mockRejectedValueOnce(new Error('File not found'));
482 | 
483 | 			await performResearch(
484 | 				'Test query',
485 | 				{ tag: 'error-tag' },
486 | 				{ mcpLog: mockLog },
487 | 				'json',
488 | 				false
489 | 			);
490 | 
491 | 			expect(mockLog.debug).toHaveBeenCalledWith(
492 | 				expect.stringContaining('Could not auto-discover tasks')
493 | 			);
494 | 		});
495 | 
496 | 		it('handles ContextGatherer constructor errors gracefully', async () => {
497 | 			ContextGatherer.mockImplementationOnce(() => {
498 | 				throw new Error('Invalid tag provided');
499 | 			});
500 | 
501 | 			await expect(
502 | 				performResearch('Test query', { tag: 'invalid-tag' }, {}, 'json', false)
503 | 			).rejects.toThrow('Invalid tag provided');
504 | 		});
505 | 
506 | 		it('handles ContextGatherer.gather errors gracefully', async () => {
507 | 			mockContextGatherer.gather.mockRejectedValueOnce(
508 | 				new Error('Gather failed')
509 | 			);
510 | 
511 | 			await expect(
512 | 				performResearch(
513 | 					'Test query',
514 | 					{ tag: 'gather-error-tag' },
515 | 					{},
516 | 					'json',
517 | 					false
518 | 				)
519 | 			).rejects.toThrow('Gather failed');
520 | 		});
521 | 	});
522 | 
523 | 	describe('MCP integration with tags', () => {
524 | 		it('uses MCP logger when mcpLog is provided in context', async () => {
525 | 			const mockMCPLog = {
526 | 				debug: jest.fn(),
527 | 				info: jest.fn(),
528 | 				warn: jest.fn(),
529 | 				error: jest.fn(),
530 | 				success: jest.fn()
531 | 			};
532 | 
533 | 			mockReadJSON.mockRejectedValueOnce(new Error('Test error'));
534 | 
535 | 			await performResearch(
536 | 				'Test query',
537 | 				{ tag: 'mcp-tag' },
538 | 				{ mcpLog: mockMCPLog },
539 | 				'json',
540 | 				false
541 | 			);
542 | 
543 | 			expect(mockMCPLog.debug).toHaveBeenCalledWith(
544 | 				expect.stringContaining('Could not auto-discover tasks')
545 | 			);
546 | 		});
547 | 
548 | 		it('passes session to generateTextService when provided', async () => {
549 | 			const mockSession = { userId: 'test-user', env: {} };
550 | 
551 | 			await performResearch(
552 | 				'Test query',
553 | 				{ tag: 'session-tag' },
554 | 				{ session: mockSession },
555 | 				'json',
556 | 				false
557 | 			);
558 | 
559 | 			expect(generateTextService).toHaveBeenCalledWith(
560 | 				expect.objectContaining({
561 | 					session: mockSession
562 | 				})
563 | 			);
564 | 		});
565 | 	});
566 | 
567 | 	describe('output format handling with tags', () => {
568 | 		it('displays UI banner only in text format', async () => {
569 | 			const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
570 | 
571 | 			await performResearch('Test query', { tag: 'ui-tag' }, {}, 'text', false);
572 | 
573 | 			expect(consoleSpy).toHaveBeenCalledWith(
574 | 				expect.stringContaining('🔍 AI Research Query')
575 | 			);
576 | 
577 | 			consoleSpy.mockRestore();
578 | 		});
579 | 
580 | 		it('does not display UI banner in json format', async () => {
581 | 			const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
582 | 
583 | 			await performResearch('Test query', { tag: 'ui-tag' }, {}, 'json', false);
584 | 
585 | 			expect(consoleSpy).not.toHaveBeenCalledWith(
586 | 				expect.stringContaining('🔍 AI Research Query')
587 | 			);
588 | 
589 | 			consoleSpy.mockRestore();
590 | 		});
591 | 	});
592 | 
593 | 	describe('comprehensive tag integration test', () => {
594 | 		it('performs complete research flow with tag-aware functionality', async () => {
595 | 			const testOptions = {
596 | 				taskIds: ['1', '2'],
597 | 				filePaths: ['src/main.js'],
598 | 				customContext: 'Testing tag integration',
599 | 				includeProjectTree: true,
600 | 				detailLevel: 'high',
601 | 				tag: 'integration-test',
602 | 				projectRoot: '/custom/root'
603 | 			};
604 | 
605 | 			const testContext = {
606 | 				session: { userId: 'test-user' },
607 | 				mcpLog: {
608 | 					debug: jest.fn(),
609 | 					info: jest.fn(),
610 | 					warn: jest.fn(),
611 | 					error: jest.fn(),
612 | 					success: jest.fn()
613 | 				},
614 | 				commandName: 'test-research',
615 | 				outputType: 'mcp'
616 | 			};
617 | 
618 | 			// Mock successful task discovery
619 | 			mockFuzzySearch.getTaskIds.mockReturnValue(['3', '4']);
620 | 
621 | 			const result = await performResearch(
622 | 				'Integration test query',
623 | 				testOptions,
624 | 				testContext,
625 | 				'json',
626 | 				false
627 | 			);
628 | 
629 | 			// Verify ContextGatherer was initialized with correct tag
630 | 			expect(ContextGatherer).toHaveBeenCalledWith(
631 | 				'/custom/root',
632 | 				'integration-test'
633 | 			);
634 | 
635 | 			// Verify readJSON was called with correct parameters
636 | 			expect(mockReadJSON).toHaveBeenCalledWith(
637 | 				expect.stringContaining('tasks.json'),
638 | 				'/custom/root',
639 | 				'integration-test'
640 | 			);
641 | 
642 | 			// Verify context gathering was called with combined tasks
643 | 			expect(mockContextGatherer.gather).toHaveBeenCalledWith({
644 | 				tasks: ['1', '2', '3', '4'],
645 | 				files: ['src/main.js'],
646 | 				customContext: 'Testing tag integration',
647 | 				includeProjectTree: true,
648 | 				format: 'research',
649 | 				includeTokenCounts: true
650 | 			});
651 | 
652 | 			// Verify AI service was called with session
653 | 			expect(generateTextService).toHaveBeenCalledWith(
654 | 				expect.objectContaining({
655 | 					session: testContext.session,
656 | 					role: 'research'
657 | 				})
658 | 			);
659 | 
660 | 			expect(result).toBeDefined();
661 | 		});
662 | 	});
663 | });
664 | 
```

--------------------------------------------------------------------------------
/tests/unit/profiles/mcp-config-validation.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { RULE_PROFILES } from '../../../src/constants/profiles.js';
  2 | import { getRulesProfile } from '../../../src/utils/rule-transformer.js';
  3 | import path from 'path';
  4 | 
  5 | describe('MCP Configuration Validation', () => {
  6 | 	describe('Profile MCP Configuration Properties', () => {
  7 | 		const expectedMcpConfigurations = {
  8 | 			amp: {
  9 | 				shouldHaveMcp: true,
 10 | 				expectedDir: '.vscode',
 11 | 				expectedConfigName: 'settings.json',
 12 | 				expectedPath: '.vscode/settings.json'
 13 | 			},
 14 | 			claude: {
 15 | 				shouldHaveMcp: true,
 16 | 				expectedDir: '.',
 17 | 				expectedConfigName: '.mcp.json',
 18 | 				expectedPath: '.mcp.json'
 19 | 			},
 20 | 			cline: {
 21 | 				shouldHaveMcp: false,
 22 | 				expectedDir: '.clinerules',
 23 | 				expectedConfigName: null,
 24 | 				expectedPath: null
 25 | 			},
 26 | 			codex: {
 27 | 				shouldHaveMcp: false,
 28 | 				expectedDir: '.',
 29 | 				expectedConfigName: null,
 30 | 				expectedPath: null
 31 | 			},
 32 | 			cursor: {
 33 | 				shouldHaveMcp: true,
 34 | 				expectedDir: '.cursor',
 35 | 				expectedConfigName: 'mcp.json',
 36 | 				expectedPath: '.cursor/mcp.json'
 37 | 			},
 38 | 			gemini: {
 39 | 				shouldHaveMcp: true,
 40 | 				expectedDir: '.gemini',
 41 | 				expectedConfigName: 'settings.json',
 42 | 				expectedPath: '.gemini/settings.json'
 43 | 			},
 44 | 			kiro: {
 45 | 				shouldHaveMcp: true,
 46 | 				expectedDir: '.kiro',
 47 | 				expectedConfigName: 'settings/mcp.json',
 48 | 				expectedPath: '.kiro/settings/mcp.json'
 49 | 			},
 50 | 			opencode: {
 51 | 				shouldHaveMcp: true,
 52 | 				expectedDir: '.',
 53 | 				expectedConfigName: 'opencode.json',
 54 | 				expectedPath: 'opencode.json'
 55 | 			},
 56 | 			roo: {
 57 | 				shouldHaveMcp: true,
 58 | 				expectedDir: '.roo',
 59 | 				expectedConfigName: 'mcp.json',
 60 | 				expectedPath: '.roo/mcp.json'
 61 | 			},
 62 | 			trae: {
 63 | 				shouldHaveMcp: false,
 64 | 				expectedDir: '.trae',
 65 | 				expectedConfigName: null,
 66 | 				expectedPath: null
 67 | 			},
 68 | 			vscode: {
 69 | 				shouldHaveMcp: true,
 70 | 				expectedDir: '.vscode',
 71 | 				expectedConfigName: 'mcp.json',
 72 | 				expectedPath: '.vscode/mcp.json'
 73 | 			},
 74 | 			windsurf: {
 75 | 				shouldHaveMcp: true,
 76 | 				expectedDir: '.windsurf',
 77 | 				expectedConfigName: 'mcp.json',
 78 | 				expectedPath: '.windsurf/mcp.json'
 79 | 			},
 80 | 			zed: {
 81 | 				shouldHaveMcp: true,
 82 | 				expectedDir: '.zed',
 83 | 				expectedConfigName: 'settings.json',
 84 | 				expectedPath: '.zed/settings.json'
 85 | 			}
 86 | 		};
 87 | 
 88 | 		Object.entries(expectedMcpConfigurations).forEach(
 89 | 			([profileName, expected]) => {
 90 | 				test(`should have correct MCP configuration for ${profileName} profile`, () => {
 91 | 					const profile = getRulesProfile(profileName);
 92 | 					expect(profile).toBeDefined();
 93 | 					expect(profile.mcpConfig).toBe(expected.shouldHaveMcp);
 94 | 					expect(profile.profileDir).toBe(expected.expectedDir);
 95 | 					expect(profile.mcpConfigName).toBe(expected.expectedConfigName);
 96 | 					expect(profile.mcpConfigPath).toBe(expected.expectedPath);
 97 | 				});
 98 | 			}
 99 | 		);
100 | 	});
101 | 
102 | 	describe('MCP Configuration Path Consistency', () => {
103 | 		test('should ensure all profiles have consistent mcpConfigPath construction', () => {
104 | 			RULE_PROFILES.forEach((profileName) => {
105 | 				const profile = getRulesProfile(profileName);
106 | 				if (profile.mcpConfig !== false) {
107 | 					// For root directory profiles, path.join('.', filename) normalizes to just 'filename'
108 | 					// except for Claude which uses '.mcp.json' explicitly
109 | 					let expectedPath;
110 | 					if (profile.profileDir === '.') {
111 | 						if (profileName === 'claude') {
112 | 							expectedPath = '.mcp.json'; // Claude explicitly uses '.mcp.json'
113 | 						} else {
114 | 							expectedPath = profile.mcpConfigName; // Other root profiles normalize to just the filename
115 | 						}
116 | 					} else {
117 | 						expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
118 | 					}
119 | 					expect(profile.mcpConfigPath).toBe(expectedPath);
120 | 				}
121 | 			});
122 | 		});
123 | 
124 | 		test('should ensure no two profiles have the same MCP config path', () => {
125 | 			const mcpPaths = new Set();
126 | 			RULE_PROFILES.forEach((profileName) => {
127 | 				const profile = getRulesProfile(profileName);
128 | 				if (profile.mcpConfig !== false) {
129 | 					expect(mcpPaths.has(profile.mcpConfigPath)).toBe(false);
130 | 					mcpPaths.add(profile.mcpConfigPath);
131 | 				}
132 | 			});
133 | 		});
134 | 
135 | 		test('should ensure all MCP-enabled profiles use proper directory structure', () => {
136 | 			const rootProfiles = ['opencode', 'claude', 'codex']; // Profiles that use root directory for config
137 | 			const nestedConfigProfiles = ['kiro']; // Profiles that use nested directories for config
138 | 
139 | 			RULE_PROFILES.forEach((profileName) => {
140 | 				const profile = getRulesProfile(profileName);
141 | 				if (profile.mcpConfig !== false) {
142 | 					if (rootProfiles.includes(profileName)) {
143 | 						// Root profiles have different patterns
144 | 						if (profileName === 'claude') {
145 | 							expect(profile.mcpConfigPath).toBe('.mcp.json');
146 | 						} else {
147 | 							// Other root profiles normalize to just the filename (no ./ prefix)
148 | 							expect(profile.mcpConfigPath).toMatch(/^[\w_.]+$/);
149 | 						}
150 | 					} else if (nestedConfigProfiles.includes(profileName)) {
151 | 						// Profiles with nested config directories
152 | 						expect(profile.mcpConfigPath).toMatch(
153 | 							/^\.[\w-]+\/[\w-]+\/[\w_.]+$/
154 | 						);
155 | 					} else {
156 | 						// Other profiles should have config files in their specific directories
157 | 						expect(profile.mcpConfigPath).toMatch(/^\.[\w-]+\/[\w_.]+$/);
158 | 					}
159 | 				}
160 | 			});
161 | 		});
162 | 
163 | 		test('should ensure all profiles have required MCP properties', () => {
164 | 			RULE_PROFILES.forEach((profileName) => {
165 | 				const profile = getRulesProfile(profileName);
166 | 				expect(profile).toHaveProperty('mcpConfig');
167 | 				expect(profile).toHaveProperty('profileDir');
168 | 				expect(profile).toHaveProperty('mcpConfigName');
169 | 				expect(profile).toHaveProperty('mcpConfigPath');
170 | 			});
171 | 		});
172 | 	});
173 | 
174 | 	describe('MCP Configuration File Names', () => {
175 | 		test('should use standard mcp.json for MCP-enabled profiles', () => {
176 | 			const standardMcpProfiles = ['cursor', 'roo', 'vscode', 'windsurf'];
177 | 			standardMcpProfiles.forEach((profileName) => {
178 | 				const profile = getRulesProfile(profileName);
179 | 				expect(profile.mcpConfigName).toBe('mcp.json');
180 | 			});
181 | 		});
182 | 
183 | 		test('should use custom settings.json for Gemini profile', () => {
184 | 			const profile = getRulesProfile('gemini');
185 | 			expect(profile.mcpConfigName).toBe('settings.json');
186 | 		});
187 | 
188 | 		test('should have null config name for non-MCP profiles', () => {
189 | 			// Only codex, cline, and trae profiles should have null config names
190 | 			const nonMcpProfiles = ['codex', 'cline', 'trae'];
191 | 
192 | 			for (const profileName of nonMcpProfiles) {
193 | 				const profile = getRulesProfile(profileName);
194 | 				expect(profile.mcpConfigName).toBe(null);
195 | 			}
196 | 		});
197 | 	});
198 | 
199 | 	describe('Profile Directory Structure', () => {
200 | 		test('should ensure each profile has a unique directory', () => {
201 | 			const profileDirs = new Set();
202 | 			// Profiles that use root directory (can share the same directory)
203 | 			const rootProfiles = ['claude', 'codex', 'gemini', 'opencode'];
204 | 			// Profiles that intentionally share the same directory
205 | 			const sharedDirectoryProfiles = ['amp', 'vscode']; // Both use .vscode
206 | 
207 | 			RULE_PROFILES.forEach((profileName) => {
208 | 				const profile = getRulesProfile(profileName);
209 | 
210 | 				// Root profiles can share the root directory for rules
211 | 				if (rootProfiles.includes(profileName) && profile.rulesDir === '.') {
212 | 					expect(profile.rulesDir).toBe('.');
213 | 				}
214 | 
215 | 				// Profile directories should be unique (except for root profiles and shared directory profiles)
216 | 				if (
217 | 					!rootProfiles.includes(profileName) &&
218 | 					!sharedDirectoryProfiles.includes(profileName)
219 | 				) {
220 | 					if (profile.profileDir !== '.') {
221 | 						expect(profileDirs.has(profile.profileDir)).toBe(false);
222 | 						profileDirs.add(profile.profileDir);
223 | 					}
224 | 				} else if (sharedDirectoryProfiles.includes(profileName)) {
225 | 					// Shared directory profiles should use .vscode
226 | 					expect(profile.profileDir).toBe('.vscode');
227 | 				}
228 | 			});
229 | 		});
230 | 
231 | 		test('should ensure profile directories follow expected naming convention', () => {
232 | 			// Profiles that use root directory for rules
233 | 			const rootRulesProfiles = ['claude', 'codex', 'gemini', 'opencode'];
234 | 
235 | 			RULE_PROFILES.forEach((profileName) => {
236 | 				const profile = getRulesProfile(profileName);
237 | 
238 | 				// Some profiles use root directory for rules
239 | 				if (
240 | 					rootRulesProfiles.includes(profileName) &&
241 | 					profile.rulesDir === '.'
242 | 				) {
243 | 					expect(profile.rulesDir).toBe('.');
244 | 				}
245 | 
246 | 				// Profile directories (not rules directories) should follow the .name pattern
247 | 				// unless they are root profiles with profileDir = '.'
248 | 				if (profile.profileDir !== '.') {
249 | 					expect(profile.profileDir).toMatch(/^\.[\w-]+$/);
250 | 				}
251 | 			});
252 | 		});
253 | 	});
254 | 
255 | 	describe('MCP Configuration Creation Logic', () => {
256 | 		test('should indicate which profiles require MCP configuration creation', () => {
257 | 			// Get all profiles that have MCP configuration enabled
258 | 			const mcpEnabledProfiles = RULE_PROFILES.filter((profileName) => {
259 | 				const profile = getRulesProfile(profileName);
260 | 				return profile.mcpConfig !== false;
261 | 			});
262 | 
263 | 			// Verify expected MCP-enabled profiles
264 | 			expect(mcpEnabledProfiles).toContain('amp');
265 | 			expect(mcpEnabledProfiles).toContain('claude');
266 | 			expect(mcpEnabledProfiles).toContain('cursor');
267 | 			expect(mcpEnabledProfiles).toContain('gemini');
268 | 			expect(mcpEnabledProfiles).toContain('opencode');
269 | 			expect(mcpEnabledProfiles).toContain('vscode');
270 | 			expect(mcpEnabledProfiles).toContain('windsurf');
271 | 			expect(mcpEnabledProfiles).toContain('zed');
272 | 			expect(mcpEnabledProfiles).toContain('roo');
273 | 			expect(mcpEnabledProfiles).not.toContain('cline');
274 | 			expect(mcpEnabledProfiles).not.toContain('codex');
275 | 			expect(mcpEnabledProfiles).not.toContain('trae');
276 | 		});
277 | 
278 | 		test('should provide all necessary information for MCP config creation', () => {
279 | 			RULE_PROFILES.forEach((profileName) => {
280 | 				const profile = getRulesProfile(profileName);
281 | 				if (profile.mcpConfig !== false) {
282 | 					expect(profile.mcpConfigPath).toBeDefined();
283 | 					expect(typeof profile.mcpConfigPath).toBe('string');
284 | 					expect(profile.mcpConfigPath.length).toBeGreaterThan(0);
285 | 				}
286 | 			});
287 | 		});
288 | 	});
289 | 
290 | 	describe('MCP Configuration Path Usage Verification', () => {
291 | 		test('should verify that rule transformer functions use mcpConfigPath correctly', () => {
292 | 			RULE_PROFILES.forEach((profileName) => {
293 | 				const profile = getRulesProfile(profileName);
294 | 				if (profile.mcpConfig !== false) {
295 | 					// Verify the path is properly formatted for path.join usage
296 | 					expect(profile.mcpConfigPath.startsWith('/')).toBe(false);
297 | 
298 | 					// Root directory profiles have different patterns
299 | 					if (profile.profileDir === '.') {
300 | 						if (profileName === 'claude') {
301 | 							expect(profile.mcpConfigPath).toBe('.mcp.json');
302 | 						} else {
303 | 							// Other root profiles (opencode) normalize to just the filename
304 | 							expect(profile.mcpConfigPath).toBe(profile.mcpConfigName);
305 | 						}
306 | 					} else {
307 | 						// Non-root profiles should contain a directory separator
308 | 						expect(profile.mcpConfigPath).toContain('/');
309 | 					}
310 | 
311 | 					// Verify it matches the expected pattern based on how path.join works
312 | 					let expectedPath;
313 | 					if (profile.profileDir === '.') {
314 | 						if (profileName === 'claude') {
315 | 							expectedPath = '.mcp.json'; // Claude explicitly uses '.mcp.json'
316 | 						} else {
317 | 							expectedPath = profile.mcpConfigName; // path.join('.', 'filename') normalizes to 'filename'
318 | 						}
319 | 					} else {
320 | 						expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
321 | 					}
322 | 					expect(profile.mcpConfigPath).toBe(expectedPath);
323 | 				}
324 | 			});
325 | 		});
326 | 
327 | 		test('should verify that mcpConfigPath is properly constructed for path.join usage', () => {
328 | 			RULE_PROFILES.forEach((profileName) => {
329 | 				const profile = getRulesProfile(profileName);
330 | 				if (profile.mcpConfig !== false) {
331 | 					// Test that path.join works correctly with the mcpConfigPath
332 | 					const testProjectRoot = '/test/project';
333 | 					const fullPath = path.join(testProjectRoot, profile.mcpConfigPath);
334 | 
335 | 					// Should result in a proper absolute path
336 | 					// Note: path.join normalizes paths, so './opencode.json' becomes 'opencode.json'
337 | 					const normalizedExpectedPath = path.join(
338 | 						testProjectRoot,
339 | 						profile.mcpConfigPath
340 | 					);
341 | 					expect(fullPath).toBe(normalizedExpectedPath);
342 | 					expect(fullPath).toContain(profile.mcpConfigName);
343 | 				}
344 | 			});
345 | 		});
346 | 	});
347 | 
348 | 	describe('MCP Configuration Function Integration', () => {
349 | 		test('should verify that setupMCPConfiguration receives the correct mcpConfigPath parameter', () => {
350 | 			RULE_PROFILES.forEach((profileName) => {
351 | 				const profile = getRulesProfile(profileName);
352 | 				if (profile.mcpConfig !== false) {
353 | 					// Verify the path structure is correct for the new function signature
354 | 					if (profile.profileDir === '.') {
355 | 						// Root directory profiles have special handling
356 | 						if (profileName === 'claude') {
357 | 							expect(profile.mcpConfigPath).toBe('.mcp.json');
358 | 						} else {
359 | 							// Other root profiles normalize to just the filename
360 | 							expect(profile.mcpConfigPath).toBe(profile.mcpConfigName);
361 | 						}
362 | 					} else if (profileName === 'kiro') {
363 | 						// Kiro has a nested config structure
364 | 						const parts = profile.mcpConfigPath.split('/');
365 | 						expect(parts).toHaveLength(3); // Should be profileDir/settings/mcp.json
366 | 						expect(parts[0]).toBe(profile.profileDir);
367 | 						expect(parts[1]).toBe('settings');
368 | 						expect(parts[2]).toBe('mcp.json');
369 | 					} else {
370 | 						// Non-root profiles should have profileDir/configName structure
371 | 						const parts = profile.mcpConfigPath.split('/');
372 | 						expect(parts).toHaveLength(2); // Should be profileDir/configName
373 | 						expect(parts[0]).toBe(profile.profileDir);
374 | 						expect(parts[1]).toBe(profile.mcpConfigName);
375 | 					}
376 | 				}
377 | 			});
378 | 		});
379 | 	});
380 | 
381 | 	describe('MCP configuration validation', () => {
382 | 		const mcpProfiles = [
383 | 			'amp',
384 | 			'claude',
385 | 			'cursor',
386 | 			'gemini',
387 | 			'kiro',
388 | 			'opencode',
389 | 			'roo',
390 | 			'windsurf',
391 | 			'vscode',
392 | 			'zed'
393 | 		];
394 | 		const nonMcpProfiles = ['codex', 'cline', 'trae'];
395 | 		const profilesWithLifecycle = ['claude'];
396 | 		const profilesWithoutLifecycle = ['codex'];
397 | 
398 | 		test.each(mcpProfiles)(
399 | 			'should have valid MCP config for %s profile',
400 | 			(profileName) => {
401 | 				const profile = getRulesProfile(profileName);
402 | 				expect(profile).toBeDefined();
403 | 				expect(profile.mcpConfig).toBe(true);
404 | 				expect(profile.mcpConfigPath).toBeDefined();
405 | 				expect(typeof profile.mcpConfigPath).toBe('string');
406 | 			}
407 | 		);
408 | 
409 | 		test.each(nonMcpProfiles)(
410 | 			'should not require MCP config for %s profile',
411 | 			(profileName) => {
412 | 				const profile = getRulesProfile(profileName);
413 | 				expect(profile).toBeDefined();
414 | 				expect(profile.mcpConfig).toBe(false);
415 | 			}
416 | 		);
417 | 	});
418 | 
419 | 	describe('Profile structure validation', () => {
420 | 		const allProfiles = [
421 | 			'amp',
422 | 			'claude',
423 | 			'cline',
424 | 			'codex',
425 | 			'cursor',
426 | 			'gemini',
427 | 			'opencode',
428 | 			'roo',
429 | 			'trae',
430 | 			'vscode',
431 | 			'windsurf',
432 | 			'zed'
433 | 		];
434 | 		const profilesWithLifecycle = ['amp', 'claude'];
435 | 		const profilesWithPostConvertLifecycle = ['opencode'];
436 | 		const profilesWithoutLifecycle = ['codex'];
437 | 
438 | 		test.each(allProfiles)(
439 | 			'should have file mappings for %s profile',
440 | 			(profileName) => {
441 | 				const profile = getRulesProfile(profileName);
442 | 				expect(profile).toBeDefined();
443 | 				expect(profile.fileMap).toBeDefined();
444 | 				expect(typeof profile.fileMap).toBe('object');
445 | 				expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
446 | 			}
447 | 		);
448 | 
449 | 		test.each(profilesWithLifecycle)(
450 | 			'should have file mappings and lifecycle functions for %s profile',
451 | 			(profileName) => {
452 | 				const profile = getRulesProfile(profileName);
453 | 				expect(profile).toBeDefined();
454 | 				// Claude profile has both fileMap and lifecycle functions
455 | 				expect(profile.fileMap).toBeDefined();
456 | 				expect(typeof profile.fileMap).toBe('object');
457 | 				expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
458 | 				expect(typeof profile.onAddRulesProfile).toBe('function');
459 | 				expect(typeof profile.onRemoveRulesProfile).toBe('function');
460 | 				expect(typeof profile.onPostConvertRulesProfile).toBe('function');
461 | 			}
462 | 		);
463 | 
464 | 		test.each(profilesWithPostConvertLifecycle)(
465 | 			'should have file mappings and post-convert lifecycle functions for %s profile',
466 | 			(profileName) => {
467 | 				const profile = getRulesProfile(profileName);
468 | 				expect(profile).toBeDefined();
469 | 				// OpenCode profile has fileMap and post-convert lifecycle functions
470 | 				expect(profile.fileMap).toBeDefined();
471 | 				expect(typeof profile.fileMap).toBe('object');
472 | 				expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
473 | 				expect(profile.onAddRulesProfile).toBeUndefined(); // OpenCode doesn't have onAdd
474 | 				expect(typeof profile.onRemoveRulesProfile).toBe('function');
475 | 				expect(typeof profile.onPostConvertRulesProfile).toBe('function');
476 | 			}
477 | 		);
478 | 
479 | 		test.each(profilesWithoutLifecycle)(
480 | 			'should have file mappings without lifecycle functions for %s profile',
481 | 			(profileName) => {
482 | 				const profile = getRulesProfile(profileName);
483 | 				expect(profile).toBeDefined();
484 | 				// Codex profile has fileMap but no lifecycle functions (simplified)
485 | 				expect(profile.fileMap).toBeDefined();
486 | 				expect(typeof profile.fileMap).toBe('object');
487 | 				expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
488 | 				expect(profile.onAddRulesProfile).toBeUndefined();
489 | 				expect(profile.onRemoveRulesProfile).toBeUndefined();
490 | 				expect(profile.onPostConvertRulesProfile).toBeUndefined();
491 | 			}
492 | 		);
493 | 	});
494 | });
495 | 
```

--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/move-task-cross-tag.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | 
  3 | // --- Mocks ---
  4 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
  5 | 	readJSON: jest.fn(),
  6 | 	writeJSON: jest.fn(),
  7 | 	log: jest.fn(),
  8 | 	setTasksForTag: jest.fn(),
  9 | 	truncate: jest.fn((t) => t),
 10 | 	isSilentMode: jest.fn(() => false),
 11 | 	traverseDependencies: jest.fn((sourceTasks, allTasks, options = {}) => {
 12 | 		// Mock realistic dependency behavior for testing
 13 | 		const { direction = 'forward' } = options;
 14 | 
 15 | 		if (direction === 'forward') {
 16 | 			// For forward dependencies: return tasks that the source tasks depend on
 17 | 			const result = [];
 18 | 			sourceTasks.forEach((task) => {
 19 | 				if (task.dependencies && Array.isArray(task.dependencies)) {
 20 | 					result.push(...task.dependencies);
 21 | 				}
 22 | 			});
 23 | 			return result;
 24 | 		} else if (direction === 'reverse') {
 25 | 			// For reverse dependencies: return tasks that depend on the source tasks
 26 | 			const sourceIds = sourceTasks.map((t) => t.id);
 27 | 			const normalizedSourceIds = sourceIds.map((id) => String(id));
 28 | 			const result = [];
 29 | 			allTasks.forEach((task) => {
 30 | 				if (task.dependencies && Array.isArray(task.dependencies)) {
 31 | 					const hasDependency = task.dependencies.some((depId) =>
 32 | 						normalizedSourceIds.includes(String(depId))
 33 | 					);
 34 | 					if (hasDependency) {
 35 | 						result.push(task.id);
 36 | 					}
 37 | 				}
 38 | 			});
 39 | 			return result;
 40 | 		}
 41 | 		return [];
 42 | 	})
 43 | }));
 44 | 
 45 | jest.unstable_mockModule(
 46 | 	'../../../../../scripts/modules/task-manager/generate-task-files.js',
 47 | 	() => ({
 48 | 		default: jest.fn().mockResolvedValue()
 49 | 	})
 50 | );
 51 | 
 52 | jest.unstable_mockModule(
 53 | 	'../../../../../scripts/modules/task-manager.js',
 54 | 	() => ({
 55 | 		isTaskDependentOn: jest.fn(() => false)
 56 | 	})
 57 | );
 58 | 
 59 | jest.unstable_mockModule(
 60 | 	'../../../../../scripts/modules/dependency-manager.js',
 61 | 	() => ({
 62 | 		validateCrossTagMove: jest.fn(),
 63 | 		findCrossTagDependencies: jest.fn(),
 64 | 		getDependentTaskIds: jest.fn(),
 65 | 		validateSubtaskMove: jest.fn()
 66 | 	})
 67 | );
 68 | 
 69 | const { readJSON, writeJSON, log } = await import(
 70 | 	'../../../../../scripts/modules/utils.js'
 71 | );
 72 | 
 73 | const {
 74 | 	validateCrossTagMove,
 75 | 	findCrossTagDependencies,
 76 | 	getDependentTaskIds,
 77 | 	validateSubtaskMove
 78 | } = await import('../../../../../scripts/modules/dependency-manager.js');
 79 | 
 80 | const { moveTasksBetweenTags, getAllTasksWithTags } = await import(
 81 | 	'../../../../../scripts/modules/task-manager/move-task.js'
 82 | );
 83 | 
 84 | describe('Cross-Tag Task Movement', () => {
 85 | 	let mockRawData;
 86 | 	let mockTasksPath;
 87 | 	let mockContext;
 88 | 
 89 | 	beforeEach(() => {
 90 | 		jest.clearAllMocks();
 91 | 
 92 | 		// Setup mock data
 93 | 		mockRawData = {
 94 | 			backlog: {
 95 | 				tasks: [
 96 | 					{ id: 1, title: 'Task 1', dependencies: [2] },
 97 | 					{ id: 2, title: 'Task 2', dependencies: [] },
 98 | 					{ id: 3, title: 'Task 3', dependencies: [1] }
 99 | 				]
100 | 			},
101 | 			'in-progress': {
102 | 				tasks: [{ id: 4, title: 'Task 4', dependencies: [] }]
103 | 			},
104 | 			done: {
105 | 				tasks: [{ id: 5, title: 'Task 5', dependencies: [4] }]
106 | 			}
107 | 		};
108 | 
109 | 		mockTasksPath = '/test/path/tasks.json';
110 | 		mockContext = { projectRoot: '/test/project' };
111 | 
112 | 		// Mock readJSON to return our test data
113 | 		readJSON.mockImplementation((path, projectRoot, tag) => {
114 | 			return { ...mockRawData[tag], tag, _rawTaggedData: mockRawData };
115 | 		});
116 | 
117 | 		writeJSON.mockResolvedValue();
118 | 		log.mockImplementation(() => {});
119 | 	});
120 | 
121 | 	afterEach(() => {
122 | 		jest.clearAllMocks();
123 | 	});
124 | 
125 | 	describe('getAllTasksWithTags', () => {
126 | 		it('should return all tasks with tag information', () => {
127 | 			const allTasks = getAllTasksWithTags(mockRawData);
128 | 
129 | 			expect(allTasks).toHaveLength(5);
130 | 			expect(allTasks.find((t) => t.id === 1).tag).toBe('backlog');
131 | 			expect(allTasks.find((t) => t.id === 4).tag).toBe('in-progress');
132 | 			expect(allTasks.find((t) => t.id === 5).tag).toBe('done');
133 | 		});
134 | 	});
135 | 
136 | 	describe('validateCrossTagMove', () => {
137 | 		it('should allow move when no dependencies exist', () => {
138 | 			const task = { id: 2, dependencies: [] };
139 | 			const allTasks = getAllTasksWithTags(mockRawData);
140 | 
141 | 			validateCrossTagMove.mockReturnValue({ canMove: true, conflicts: [] });
142 | 			const result = validateCrossTagMove(
143 | 				task,
144 | 				'backlog',
145 | 				'in-progress',
146 | 				allTasks
147 | 			);
148 | 
149 | 			expect(result.canMove).toBe(true);
150 | 			expect(result.conflicts).toHaveLength(0);
151 | 		});
152 | 
153 | 		it('should block move when cross-tag dependencies exist', () => {
154 | 			const task = { id: 1, dependencies: [2] };
155 | 			const allTasks = getAllTasksWithTags(mockRawData);
156 | 
157 | 			validateCrossTagMove.mockReturnValue({
158 | 				canMove: false,
159 | 				conflicts: [{ taskId: 1, dependencyId: 2, dependencyTag: 'backlog' }]
160 | 			});
161 | 			const result = validateCrossTagMove(
162 | 				task,
163 | 				'backlog',
164 | 				'in-progress',
165 | 				allTasks
166 | 			);
167 | 
168 | 			expect(result.canMove).toBe(false);
169 | 			expect(result.conflicts).toHaveLength(1);
170 | 			expect(result.conflicts[0].dependencyId).toBe(2);
171 | 		});
172 | 	});
173 | 
174 | 	describe('findCrossTagDependencies', () => {
175 | 		it('should find cross-tag dependencies for multiple tasks', () => {
176 | 			const sourceTasks = [
177 | 				{ id: 1, dependencies: [2] },
178 | 				{ id: 3, dependencies: [1] }
179 | 			];
180 | 			const allTasks = getAllTasksWithTags(mockRawData);
181 | 
182 | 			findCrossTagDependencies.mockReturnValue([
183 | 				{ taskId: 1, dependencyId: 2, dependencyTag: 'backlog' },
184 | 				{ taskId: 3, dependencyId: 1, dependencyTag: 'backlog' }
185 | 			]);
186 | 			const conflicts = findCrossTagDependencies(
187 | 				sourceTasks,
188 | 				'backlog',
189 | 				'in-progress',
190 | 				allTasks
191 | 			);
192 | 
193 | 			expect(conflicts).toHaveLength(2);
194 | 			expect(
195 | 				conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
196 | 			).toBe(true);
197 | 			expect(
198 | 				conflicts.some((c) => c.taskId === 3 && c.dependencyId === 1)
199 | 			).toBe(true);
200 | 		});
201 | 	});
202 | 
203 | 	describe('getDependentTaskIds', () => {
204 | 		it('should return dependent task IDs', () => {
205 | 			const sourceTasks = [{ id: 1, dependencies: [2] }];
206 | 			const crossTagDependencies = [
207 | 				{ taskId: 1, dependencyId: 2, dependencyTag: 'backlog' }
208 | 			];
209 | 			const allTasks = getAllTasksWithTags(mockRawData);
210 | 
211 | 			getDependentTaskIds.mockReturnValue([2]);
212 | 			const dependentTaskIds = getDependentTaskIds(
213 | 				sourceTasks,
214 | 				crossTagDependencies,
215 | 				allTasks
216 | 			);
217 | 
218 | 			expect(dependentTaskIds).toContain(2);
219 | 		});
220 | 	});
221 | 
222 | 	// New test: ensure with-dependencies only traverses tasks from the source tag
223 | 	it('should scope dependency traversal to source tag when using --with-dependencies', async () => {
224 | 		findCrossTagDependencies.mockReturnValue([]);
225 | 		validateSubtaskMove.mockImplementation(() => {});
226 | 
227 | 		const result = await moveTasksBetweenTags(
228 | 			mockTasksPath,
229 | 			[1], // backlog:1 depends on backlog:2
230 | 			'backlog',
231 | 			'in-progress',
232 | 			{ withDependencies: true },
233 | 			mockContext
234 | 		);
235 | 
236 | 		// Write should include backlog:2 moved, and must NOT traverse or fetch dependencies from the target tag
237 | 		expect(writeJSON).toHaveBeenCalledWith(
238 | 			mockTasksPath,
239 | 			expect.objectContaining({
240 | 				'in-progress': expect.objectContaining({
241 | 					tasks: expect.arrayContaining([
242 | 						expect.objectContaining({ id: 1 }),
243 | 						expect.objectContaining({ id: 2 }) // the backlog:2 now moved
244 | 						// ensure existing in-progress:2 remains (by id) but we don't double-add or fetch deps from it
245 | 					])
246 | 				})
247 | 			}),
248 | 			mockContext.projectRoot,
249 | 			null
250 | 		);
251 | 	});
252 | 
253 | 	describe('moveTasksBetweenTags', () => {
254 | 		it('should move tasks without dependencies successfully', async () => {
255 | 			// Mock the dependency functions to return no conflicts
256 | 			findCrossTagDependencies.mockReturnValue([]);
257 | 			validateSubtaskMove.mockImplementation(() => {});
258 | 
259 | 			const result = await moveTasksBetweenTags(
260 | 				mockTasksPath,
261 | 				[2],
262 | 				'backlog',
263 | 				'in-progress',
264 | 				{},
265 | 				mockContext
266 | 			);
267 | 
268 | 			expect(result.message).toContain('Successfully moved 1 tasks');
269 | 			expect(writeJSON).toHaveBeenCalledWith(
270 | 				mockTasksPath,
271 | 				expect.any(Object),
272 | 				mockContext.projectRoot,
273 | 				null
274 | 			);
275 | 		});
276 | 
277 | 		it('should throw error for cross-tag dependencies by default', async () => {
278 | 			const mockDependency = {
279 | 				taskId: 1,
280 | 				dependencyId: 2,
281 | 				dependencyTag: 'backlog'
282 | 			};
283 | 			findCrossTagDependencies.mockReturnValue([mockDependency]);
284 | 			validateSubtaskMove.mockImplementation(() => {});
285 | 
286 | 			await expect(
287 | 				moveTasksBetweenTags(
288 | 					mockTasksPath,
289 | 					[1],
290 | 					'backlog',
291 | 					'in-progress',
292 | 					{},
293 | 					mockContext
294 | 				)
295 | 			).rejects.toThrow(
296 | 				'Cannot move tasks: 1 cross-tag dependency conflicts found'
297 | 			);
298 | 
299 | 			expect(writeJSON).not.toHaveBeenCalled();
300 | 		});
301 | 
302 | 		it('should move with dependencies when --with-dependencies is used', async () => {
303 | 			const mockDependency = {
304 | 				taskId: 1,
305 | 				dependencyId: 2,
306 | 				dependencyTag: 'backlog'
307 | 			};
308 | 			findCrossTagDependencies.mockReturnValue([mockDependency]);
309 | 			getDependentTaskIds.mockReturnValue([2]);
310 | 			validateSubtaskMove.mockImplementation(() => {});
311 | 
312 | 			const result = await moveTasksBetweenTags(
313 | 				mockTasksPath,
314 | 				[1],
315 | 				'backlog',
316 | 				'in-progress',
317 | 				{ withDependencies: true },
318 | 				mockContext
319 | 			);
320 | 
321 | 			expect(result.message).toContain('Successfully moved 2 tasks');
322 | 			expect(writeJSON).toHaveBeenCalledWith(
323 | 				mockTasksPath,
324 | 				expect.objectContaining({
325 | 					backlog: expect.objectContaining({
326 | 						tasks: expect.arrayContaining([
327 | 							expect.objectContaining({
328 | 								id: 3,
329 | 								title: 'Task 3',
330 | 								dependencies: [1]
331 | 							})
332 | 						])
333 | 					}),
334 | 					'in-progress': expect.objectContaining({
335 | 						tasks: expect.arrayContaining([
336 | 							expect.objectContaining({
337 | 								id: 4,
338 | 								title: 'Task 4',
339 | 								dependencies: []
340 | 							}),
341 | 							expect.objectContaining({
342 | 								id: 1,
343 | 								title: 'Task 1',
344 | 								dependencies: [2],
345 | 								metadata: expect.objectContaining({
346 | 									moveHistory: expect.arrayContaining([
347 | 										expect.objectContaining({
348 | 											fromTag: 'backlog',
349 | 											toTag: 'in-progress',
350 | 											timestamp: expect.any(String)
351 | 										})
352 | 									])
353 | 								})
354 | 							}),
355 | 							expect.objectContaining({
356 | 								id: 2,
357 | 								title: 'Task 2',
358 | 								dependencies: [],
359 | 								metadata: expect.objectContaining({
360 | 									moveHistory: expect.arrayContaining([
361 | 										expect.objectContaining({
362 | 											fromTag: 'backlog',
363 | 											toTag: 'in-progress',
364 | 											timestamp: expect.any(String)
365 | 										})
366 | 									])
367 | 								})
368 | 							})
369 | 						])
370 | 					}),
371 | 					done: expect.objectContaining({
372 | 						tasks: expect.arrayContaining([
373 | 							expect.objectContaining({
374 | 								id: 5,
375 | 								title: 'Task 5',
376 | 								dependencies: [4]
377 | 							})
378 | 						])
379 | 					})
380 | 				}),
381 | 				mockContext.projectRoot,
382 | 				null
383 | 			);
384 | 		});
385 | 
386 | 		it('should break dependencies when --ignore-dependencies is used', async () => {
387 | 			const mockDependency = {
388 | 				taskId: 1,
389 | 				dependencyId: 2,
390 | 				dependencyTag: 'backlog'
391 | 			};
392 | 			findCrossTagDependencies.mockReturnValue([mockDependency]);
393 | 			validateSubtaskMove.mockImplementation(() => {});
394 | 
395 | 			const result = await moveTasksBetweenTags(
396 | 				mockTasksPath,
397 | 				[2],
398 | 				'backlog',
399 | 				'in-progress',
400 | 				{ ignoreDependencies: true },
401 | 				mockContext
402 | 			);
403 | 
404 | 			expect(result.message).toContain('Successfully moved 1 tasks');
405 | 			expect(writeJSON).toHaveBeenCalledWith(
406 | 				mockTasksPath,
407 | 				expect.objectContaining({
408 | 					backlog: expect.objectContaining({
409 | 						tasks: expect.arrayContaining([
410 | 							expect.objectContaining({
411 | 								id: 1,
412 | 								title: 'Task 1',
413 | 								dependencies: [2] // Dependencies not actually removed in current implementation
414 | 							}),
415 | 							expect.objectContaining({
416 | 								id: 3,
417 | 								title: 'Task 3',
418 | 								dependencies: [1]
419 | 							})
420 | 						])
421 | 					}),
422 | 					'in-progress': expect.objectContaining({
423 | 						tasks: expect.arrayContaining([
424 | 							expect.objectContaining({
425 | 								id: 4,
426 | 								title: 'Task 4',
427 | 								dependencies: []
428 | 							}),
429 | 							expect.objectContaining({
430 | 								id: 2,
431 | 								title: 'Task 2',
432 | 								dependencies: [],
433 | 								metadata: expect.objectContaining({
434 | 									moveHistory: expect.arrayContaining([
435 | 										expect.objectContaining({
436 | 											fromTag: 'backlog',
437 | 											toTag: 'in-progress',
438 | 											timestamp: expect.any(String)
439 | 										})
440 | 									])
441 | 								})
442 | 							})
443 | 						])
444 | 					}),
445 | 					done: expect.objectContaining({
446 | 						tasks: expect.arrayContaining([
447 | 							expect.objectContaining({
448 | 								id: 5,
449 | 								title: 'Task 5',
450 | 								dependencies: [4]
451 | 							})
452 | 						])
453 | 					})
454 | 				}),
455 | 				mockContext.projectRoot,
456 | 				null
457 | 			);
458 | 		});
459 | 
460 | 		it('should create target tag if it does not exist', async () => {
461 | 			findCrossTagDependencies.mockReturnValue([]);
462 | 			validateSubtaskMove.mockImplementation(() => {});
463 | 
464 | 			const result = await moveTasksBetweenTags(
465 | 				mockTasksPath,
466 | 				[2],
467 | 				'backlog',
468 | 				'new-tag',
469 | 				{},
470 | 				mockContext
471 | 			);
472 | 
473 | 			expect(result.message).toContain('Successfully moved 1 tasks');
474 | 			expect(result.message).toContain('new-tag');
475 | 			expect(writeJSON).toHaveBeenCalledWith(
476 | 				mockTasksPath,
477 | 				expect.objectContaining({
478 | 					backlog: expect.objectContaining({
479 | 						tasks: expect.arrayContaining([
480 | 							expect.objectContaining({
481 | 								id: 1,
482 | 								title: 'Task 1',
483 | 								dependencies: [2]
484 | 							}),
485 | 							expect.objectContaining({
486 | 								id: 3,
487 | 								title: 'Task 3',
488 | 								dependencies: [1]
489 | 							})
490 | 						])
491 | 					}),
492 | 					'new-tag': expect.objectContaining({
493 | 						tasks: expect.arrayContaining([
494 | 							expect.objectContaining({
495 | 								id: 2,
496 | 								title: 'Task 2',
497 | 								dependencies: [],
498 | 								metadata: expect.objectContaining({
499 | 									moveHistory: expect.arrayContaining([
500 | 										expect.objectContaining({
501 | 											fromTag: 'backlog',
502 | 											toTag: 'new-tag',
503 | 											timestamp: expect.any(String)
504 | 										})
505 | 									])
506 | 								})
507 | 							})
508 | 						])
509 | 					}),
510 | 					'in-progress': expect.objectContaining({
511 | 						tasks: expect.arrayContaining([
512 | 							expect.objectContaining({
513 | 								id: 4,
514 | 								title: 'Task 4',
515 | 								dependencies: []
516 | 							})
517 | 						])
518 | 					}),
519 | 					done: expect.objectContaining({
520 | 						tasks: expect.arrayContaining([
521 | 							expect.objectContaining({
522 | 								id: 5,
523 | 								title: 'Task 5',
524 | 								dependencies: [4]
525 | 							})
526 | 						])
527 | 					})
528 | 				}),
529 | 				mockContext.projectRoot,
530 | 				null
531 | 			);
532 | 		});
533 | 
534 | 		it('should throw error for subtask movement', async () => {
535 | 			const subtaskError = 'Cannot move subtask 1.2 directly between tags';
536 | 			validateSubtaskMove.mockImplementation(() => {
537 | 				throw new Error(subtaskError);
538 | 			});
539 | 
540 | 			await expect(
541 | 				moveTasksBetweenTags(
542 | 					mockTasksPath,
543 | 					['1.2'],
544 | 					'backlog',
545 | 					'in-progress',
546 | 					{},
547 | 					mockContext
548 | 				)
549 | 			).rejects.toThrow(subtaskError);
550 | 
551 | 			expect(writeJSON).not.toHaveBeenCalled();
552 | 		});
553 | 
554 | 		it('should throw error for invalid task IDs', async () => {
555 | 			findCrossTagDependencies.mockReturnValue([]);
556 | 			validateSubtaskMove.mockImplementation(() => {});
557 | 
558 | 			await expect(
559 | 				moveTasksBetweenTags(
560 | 					mockTasksPath,
561 | 					[999], // Non-existent task
562 | 					'backlog',
563 | 					'in-progress',
564 | 					{},
565 | 					mockContext
566 | 				)
567 | 			).rejects.toThrow('Task 999 not found in source tag "backlog"');
568 | 
569 | 			expect(writeJSON).not.toHaveBeenCalled();
570 | 		});
571 | 
572 | 		it('should throw error for invalid source tag', async () => {
573 | 			findCrossTagDependencies.mockReturnValue([]);
574 | 			validateSubtaskMove.mockImplementation(() => {});
575 | 
576 | 			await expect(
577 | 				moveTasksBetweenTags(
578 | 					mockTasksPath,
579 | 					[1],
580 | 					'non-existent-tag',
581 | 					'in-progress',
582 | 					{},
583 | 					mockContext
584 | 				)
585 | 			).rejects.toThrow('Source tag "non-existent-tag" not found or invalid');
586 | 
587 | 			expect(writeJSON).not.toHaveBeenCalled();
588 | 		});
589 | 
590 | 		it('should handle string dependencies correctly during cross-tag move', async () => {
591 | 			// Setup mock data with string dependencies
592 | 			mockRawData = {
593 | 				backlog: {
594 | 					tasks: [
595 | 						{ id: 1, title: 'Task 1', dependencies: ['2'] }, // String dependency
596 | 						{ id: 2, title: 'Task 2', dependencies: [] },
597 | 						{ id: 3, title: 'Task 3', dependencies: ['1'] } // String dependency
598 | 					]
599 | 				},
600 | 				'in-progress': {
601 | 					tasks: [{ id: 4, title: 'Task 4', dependencies: [] }]
602 | 				}
603 | 			};
604 | 
605 | 			// Mock readJSON to return our test data
606 | 			readJSON.mockImplementation((path, projectRoot, tag) => {
607 | 				return { ...mockRawData[tag], tag, _rawTaggedData: mockRawData };
608 | 			});
609 | 
610 | 			findCrossTagDependencies.mockReturnValue([]);
611 | 			validateSubtaskMove.mockImplementation(() => {});
612 | 
613 | 			const result = await moveTasksBetweenTags(
614 | 				mockTasksPath,
615 | 				['1'], // String task ID
616 | 				'backlog',
617 | 				'in-progress',
618 | 				{},
619 | 				mockContext
620 | 			);
621 | 
622 | 			expect(result.message).toContain('Successfully moved 1 tasks');
623 | 			expect(writeJSON).toHaveBeenCalledWith(
624 | 				mockTasksPath,
625 | 				expect.objectContaining({
626 | 					backlog: expect.objectContaining({
627 | 						tasks: expect.arrayContaining([
628 | 							expect.objectContaining({
629 | 								id: 2,
630 | 								title: 'Task 2',
631 | 								dependencies: []
632 | 							}),
633 | 							expect.objectContaining({
634 | 								id: 3,
635 | 								title: 'Task 3',
636 | 								dependencies: ['1'] // Should remain as string
637 | 							})
638 | 						])
639 | 					}),
640 | 					'in-progress': expect.objectContaining({
641 | 						tasks: expect.arrayContaining([
642 | 							expect.objectContaining({
643 | 								id: 1,
644 | 								title: 'Task 1',
645 | 								dependencies: ['2'], // Should remain as string
646 | 								metadata: expect.objectContaining({
647 | 									moveHistory: expect.arrayContaining([
648 | 										expect.objectContaining({
649 | 											fromTag: 'backlog',
650 | 											toTag: 'in-progress',
651 | 											timestamp: expect.any(String)
652 | 										})
653 | 									])
654 | 								})
655 | 							})
656 | 						])
657 | 					})
658 | 				}),
659 | 				mockContext.projectRoot,
660 | 				null
661 | 			);
662 | 		});
663 | 	});
664 | });
665 | 
```

--------------------------------------------------------------------------------
/tests/integration/move-task-simple.integration.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | import path from 'path';
  3 | import mockFs from 'mock-fs';
  4 | import fs from 'fs';
  5 | import { fileURLToPath } from 'url';
  6 | 
  7 | // Import the actual move task functionality
  8 | import moveTask, {
  9 | 	moveTasksBetweenTags
 10 | } from '../../scripts/modules/task-manager/move-task.js';
 11 | import { readJSON, writeJSON } from '../../scripts/modules/utils.js';
 12 | 
 13 | // Mock console to avoid conflicts with mock-fs
 14 | const originalConsole = { ...console };
 15 | beforeAll(() => {
 16 | 	global.console = {
 17 | 		...console,
 18 | 		log: jest.fn(),
 19 | 		error: jest.fn(),
 20 | 		warn: jest.fn(),
 21 | 		info: jest.fn()
 22 | 	};
 23 | });
 24 | 
 25 | afterAll(() => {
 26 | 	global.console = originalConsole;
 27 | });
 28 | 
 29 | // Get __dirname equivalent for ES modules
 30 | const __filename = fileURLToPath(import.meta.url);
 31 | const __dirname = path.dirname(__filename);
 32 | 
 33 | describe('Cross-Tag Task Movement Simple Integration Tests', () => {
 34 | 	const testDataDir = path.join(__dirname, 'fixtures');
 35 | 	const testTasksPath = path.join(testDataDir, 'tasks.json');
 36 | 
 37 | 	// Test data structure with proper tagged format
 38 | 	const testData = {
 39 | 		backlog: {
 40 | 			tasks: [
 41 | 				{ id: 1, title: 'Task 1', dependencies: [], status: 'pending' },
 42 | 				{ id: 2, title: 'Task 2', dependencies: [], status: 'pending' }
 43 | 			]
 44 | 		},
 45 | 		'in-progress': {
 46 | 			tasks: [
 47 | 				{ id: 3, title: 'Task 3', dependencies: [], status: 'in-progress' }
 48 | 			]
 49 | 		}
 50 | 	};
 51 | 
 52 | 	beforeEach(() => {
 53 | 		// Set up mock file system with test data
 54 | 		mockFs({
 55 | 			[testDataDir]: {
 56 | 				'tasks.json': JSON.stringify(testData, null, 2)
 57 | 			}
 58 | 		});
 59 | 	});
 60 | 
 61 | 	afterEach(() => {
 62 | 		// Clean up mock file system
 63 | 		mockFs.restore();
 64 | 	});
 65 | 
 66 | 	describe('Real Module Integration Tests', () => {
 67 | 		it('should move task within same tag using actual moveTask function', async () => {
 68 | 			// Test moving Task 1 from position 1 to position 5 within backlog tag
 69 | 			const result = await moveTask(
 70 | 				testTasksPath,
 71 | 				'1',
 72 | 				'5',
 73 | 				false, // Don't generate files for this test
 74 | 				{ tag: 'backlog' }
 75 | 			);
 76 | 
 77 | 			// Verify the move operation was successful
 78 | 			expect(result).toBeDefined();
 79 | 			expect(result.message).toContain('Moved task 1 to new ID 5');
 80 | 
 81 | 			// Read the updated data to verify the move actually happened
 82 | 			const updatedData = readJSON(testTasksPath, null, 'backlog');
 83 | 			const rawData = updatedData._rawTaggedData || updatedData;
 84 | 			const backlogTasks = rawData.backlog.tasks;
 85 | 
 86 | 			// Verify Task 1 is no longer at position 1
 87 | 			const taskAtPosition1 = backlogTasks.find((t) => t.id === 1);
 88 | 			expect(taskAtPosition1).toBeUndefined();
 89 | 
 90 | 			// Verify Task 1 is now at position 5
 91 | 			const taskAtPosition5 = backlogTasks.find((t) => t.id === 5);
 92 | 			expect(taskAtPosition5).toBeDefined();
 93 | 			expect(taskAtPosition5.title).toBe('Task 1');
 94 | 			expect(taskAtPosition5.status).toBe('pending');
 95 | 		});
 96 | 
 97 | 		it('should move tasks between tags using moveTasksBetweenTags function', async () => {
 98 | 			// Test moving Task 1 from backlog to in-progress tag
 99 | 			const result = await moveTasksBetweenTags(
100 | 				testTasksPath,
101 | 				['1'], // Task IDs to move (as strings)
102 | 				'backlog', // Source tag
103 | 				'in-progress', // Target tag
104 | 				{ withDependencies: false, ignoreDependencies: false },
105 | 				{ projectRoot: testDataDir }
106 | 			);
107 | 
108 | 			// Verify the cross-tag move operation was successful
109 | 			expect(result).toBeDefined();
110 | 			expect(result.message).toContain(
111 | 				'Successfully moved 1 tasks from "backlog" to "in-progress"'
112 | 			);
113 | 			expect(result.movedTasks).toHaveLength(1);
114 | 			expect(result.movedTasks[0].id).toBe('1');
115 | 			expect(result.movedTasks[0].fromTag).toBe('backlog');
116 | 			expect(result.movedTasks[0].toTag).toBe('in-progress');
117 | 
118 | 			// Read the updated data to verify the move actually happened
119 | 			const updatedData = readJSON(testTasksPath, null, 'backlog');
120 | 			// readJSON returns resolved data, so we need to access the raw tagged data
121 | 			const rawData = updatedData._rawTaggedData || updatedData;
122 | 			const backlogTasks = rawData.backlog?.tasks || [];
123 | 			const inProgressTasks = rawData['in-progress']?.tasks || [];
124 | 
125 | 			// Verify Task 1 is no longer in backlog
126 | 			const taskInBacklog = backlogTasks.find((t) => t.id === 1);
127 | 			expect(taskInBacklog).toBeUndefined();
128 | 
129 | 			// Verify Task 1 is now in in-progress
130 | 			const taskInProgress = inProgressTasks.find((t) => t.id === 1);
131 | 			expect(taskInProgress).toBeDefined();
132 | 			expect(taskInProgress.title).toBe('Task 1');
133 | 			expect(taskInProgress.status).toBe('pending');
134 | 		});
135 | 
136 | 		it('should handle subtask movement restrictions', async () => {
137 | 			// Create data with subtasks
138 | 			const dataWithSubtasks = {
139 | 				backlog: {
140 | 					tasks: [
141 | 						{
142 | 							id: 1,
143 | 							title: 'Task 1',
144 | 							dependencies: [],
145 | 							status: 'pending',
146 | 							subtasks: [
147 | 								{ id: '1.1', title: 'Subtask 1.1', status: 'pending' },
148 | 								{ id: '1.2', title: 'Subtask 1.2', status: 'pending' }
149 | 							]
150 | 						}
151 | 					]
152 | 				},
153 | 				'in-progress': {
154 | 					tasks: [
155 | 						{ id: 2, title: 'Task 2', dependencies: [], status: 'in-progress' }
156 | 					]
157 | 				}
158 | 			};
159 | 
160 | 			// Write subtask data to mock file system
161 | 			mockFs({
162 | 				[testDataDir]: {
163 | 					'tasks.json': JSON.stringify(dataWithSubtasks, null, 2)
164 | 				}
165 | 			});
166 | 
167 | 			// Try to move a subtask directly - this should actually work (converts subtask to task)
168 | 			const result = await moveTask(
169 | 				testTasksPath,
170 | 				'1.1', // Subtask ID
171 | 				'5', // New task ID
172 | 				false,
173 | 				{ tag: 'backlog' }
174 | 			);
175 | 
176 | 			// Verify the subtask was converted to a task
177 | 			expect(result).toBeDefined();
178 | 			expect(result.message).toContain('Converted subtask 1.1 to task 5');
179 | 
180 | 			// Verify the subtask was removed from the parent and converted to a standalone task
181 | 			const updatedData = readJSON(testTasksPath, null, 'backlog');
182 | 			const rawData = updatedData._rawTaggedData || updatedData;
183 | 			const task1 = rawData.backlog?.tasks?.find((t) => t.id === 1);
184 | 			const newTask5 = rawData.backlog?.tasks?.find((t) => t.id === 5);
185 | 
186 | 			expect(task1).toBeDefined();
187 | 			expect(task1.subtasks).toHaveLength(1); // Only 1.2 remains
188 | 			expect(task1.subtasks[0].id).toBe(2);
189 | 
190 | 			expect(newTask5).toBeDefined();
191 | 			expect(newTask5.title).toBe('Subtask 1.1');
192 | 			expect(newTask5.status).toBe('pending');
193 | 		});
194 | 
195 | 		it('should handle missing source tag errors', async () => {
196 | 			// Try to move from a non-existent tag
197 | 			await expect(
198 | 				moveTasksBetweenTags(
199 | 					testTasksPath,
200 | 					['1'],
201 | 					'non-existent-tag', // Source tag doesn't exist
202 | 					'in-progress',
203 | 					{ withDependencies: false, ignoreDependencies: false },
204 | 					{ projectRoot: testDataDir }
205 | 				)
206 | 			).rejects.toThrow();
207 | 		});
208 | 
209 | 		it('should handle missing task ID errors', async () => {
210 | 			// Try to move a non-existent task
211 | 			await expect(
212 | 				moveTask(
213 | 					testTasksPath,
214 | 					'999', // Non-existent task ID
215 | 					'5',
216 | 					false,
217 | 					{ tag: 'backlog' }
218 | 				)
219 | 			).rejects.toThrow();
220 | 		});
221 | 
222 | 		it('should handle ignoreDependencies option correctly', async () => {
223 | 			// Create data with dependencies
224 | 			const dataWithDependencies = {
225 | 				backlog: {
226 | 					tasks: [
227 | 						{ id: 1, title: 'Task 1', dependencies: [2], status: 'pending' },
228 | 						{ id: 2, title: 'Task 2', dependencies: [], status: 'pending' }
229 | 					]
230 | 				},
231 | 				'in-progress': {
232 | 					tasks: [
233 | 						{ id: 3, title: 'Task 3', dependencies: [], status: 'in-progress' }
234 | 					]
235 | 				}
236 | 			};
237 | 
238 | 			// Write dependency data to mock file system
239 | 			mockFs({
240 | 				[testDataDir]: {
241 | 					'tasks.json': JSON.stringify(dataWithDependencies, null, 2)
242 | 				}
243 | 			});
244 | 
245 | 			// Move Task 1 while ignoring its dependencies
246 | 			const result = await moveTasksBetweenTags(
247 | 				testTasksPath,
248 | 				['1'], // Only Task 1
249 | 				'backlog',
250 | 				'in-progress',
251 | 				{ withDependencies: false, ignoreDependencies: true },
252 | 				{ projectRoot: testDataDir }
253 | 			);
254 | 
255 | 			expect(result).toBeDefined();
256 | 			expect(result.movedTasks).toHaveLength(1);
257 | 
258 | 			// Verify Task 1 moved but Task 2 stayed
259 | 			const updatedData = readJSON(testTasksPath, null, 'backlog');
260 | 			const rawData = updatedData._rawTaggedData || updatedData;
261 | 			expect(rawData.backlog.tasks).toHaveLength(1); // Task 2 remains
262 | 			expect(rawData['in-progress'].tasks).toHaveLength(2); // Task 3 + Task 1
263 | 
264 | 			// Verify Task 1 has no dependencies (they were ignored)
265 | 			const movedTask = rawData['in-progress'].tasks.find((t) => t.id === 1);
266 | 			expect(movedTask.dependencies).toEqual([]);
267 | 		});
268 | 	});
269 | 
270 | 	describe('Complex Dependency Scenarios', () => {
271 | 		beforeAll(() => {
272 | 			// Document the mock-fs limitation for complex dependency scenarios
273 | 			console.warn(
274 | 				'⚠️  Complex dependency tests are skipped due to mock-fs limitations. ' +
275 | 					'These tests require real filesystem operations for proper dependency resolution. ' +
276 | 					'Consider using real temporary filesystem setup for these scenarios.'
277 | 			);
278 | 		});
279 | 
280 | 		it.skip('should handle dependency conflicts during cross-tag moves', async () => {
281 | 			// For now, skip this test as the mock setup is not working correctly
282 | 			// TODO: Fix mock-fs setup for complex dependency scenarios
283 | 		});
284 | 
285 | 		it.skip('should handle withDependencies option correctly', async () => {
286 | 			// For now, skip this test as the mock setup is not working correctly
287 | 			// TODO: Fix mock-fs setup for complex dependency scenarios
288 | 		});
289 | 	});
290 | 
291 | 	describe('Complex Dependency Integration Tests with Mock-fs', () => {
292 | 		const complexTestData = {
293 | 			backlog: {
294 | 				tasks: [
295 | 					{ id: 1, title: 'Task 1', dependencies: [2, 3], status: 'pending' },
296 | 					{ id: 2, title: 'Task 2', dependencies: [4], status: 'pending' },
297 | 					{ id: 3, title: 'Task 3', dependencies: [], status: 'pending' },
298 | 					{ id: 4, title: 'Task 4', dependencies: [], status: 'pending' }
299 | 				]
300 | 			},
301 | 			'in-progress': {
302 | 				tasks: [
303 | 					{ id: 5, title: 'Task 5', dependencies: [], status: 'in-progress' }
304 | 				]
305 | 			}
306 | 		};
307 | 
308 | 		beforeEach(() => {
309 | 			// Set up mock file system with complex dependency data
310 | 			mockFs({
311 | 				[testDataDir]: {
312 | 					'tasks.json': JSON.stringify(complexTestData, null, 2)
313 | 				}
314 | 			});
315 | 		});
316 | 
317 | 		afterEach(() => {
318 | 			// Clean up mock file system
319 | 			mockFs.restore();
320 | 		});
321 | 
322 | 		it('should handle dependency conflicts during cross-tag moves using actual move functions', async () => {
323 | 			// Test moving Task 1 which has dependencies on Tasks 2 and 3
324 | 			// This should fail because Task 1 depends on Tasks 2 and 3 which are in the same tag
325 | 			await expect(
326 | 				moveTasksBetweenTags(
327 | 					testTasksPath,
328 | 					['1'], // Task 1 with dependencies
329 | 					'backlog',
330 | 					'in-progress',
331 | 					{ withDependencies: false, ignoreDependencies: false },
332 | 					{ projectRoot: testDataDir }
333 | 				)
334 | 			).rejects.toThrow(
335 | 				'Cannot move tasks: 2 cross-tag dependency conflicts found'
336 | 			);
337 | 		});
338 | 
339 | 		it('should handle withDependencies option correctly using actual move functions', async () => {
340 | 			// Test moving Task 1 with its dependencies (Tasks 2 and 3)
341 | 			// Task 2 also depends on Task 4, so all 4 tasks should move
342 | 			const result = await moveTasksBetweenTags(
343 | 				testTasksPath,
344 | 				['1'], // Task 1
345 | 				'backlog',
346 | 				'in-progress',
347 | 				{ withDependencies: true, ignoreDependencies: false },
348 | 				{ projectRoot: testDataDir }
349 | 			);
350 | 
351 | 			// Verify the move operation was successful
352 | 			expect(result).toBeDefined();
353 | 			expect(result.message).toContain(
354 | 				'Successfully moved 4 tasks from "backlog" to "in-progress"'
355 | 			);
356 | 			expect(result.movedTasks).toHaveLength(4); // Task 1 + Tasks 2, 3, 4
357 | 
358 | 			// Read the updated data to verify all dependent tasks moved
359 | 			const updatedData = readJSON(testTasksPath, null, 'backlog');
360 | 			const rawData = updatedData._rawTaggedData || updatedData;
361 | 
362 | 			// Verify all tasks moved from backlog
363 | 			expect(rawData.backlog?.tasks || []).toHaveLength(0); // All tasks moved
364 | 
365 | 			// Verify all tasks are now in in-progress
366 | 			expect(rawData['in-progress']?.tasks || []).toHaveLength(5); // Task 5 + Tasks 1, 2, 3, 4
367 | 
368 | 			// Verify dependency relationships are preserved
369 | 			const task1 = rawData['in-progress']?.tasks?.find((t) => t.id === 1);
370 | 			const task2 = rawData['in-progress']?.tasks?.find((t) => t.id === 2);
371 | 			const task3 = rawData['in-progress']?.tasks?.find((t) => t.id === 3);
372 | 			const task4 = rawData['in-progress']?.tasks?.find((t) => t.id === 4);
373 | 
374 | 			expect(task1?.dependencies).toEqual([2, 3]);
375 | 			expect(task2?.dependencies).toEqual([4]);
376 | 			expect(task3?.dependencies).toEqual([]);
377 | 			expect(task4?.dependencies).toEqual([]);
378 | 		});
379 | 
380 | 		it('should handle circular dependency detection using actual move functions', async () => {
381 | 			// Create data with circular dependencies
382 | 			const circularData = {
383 | 				backlog: {
384 | 					tasks: [
385 | 						{ id: 1, title: 'Task 1', dependencies: [2], status: 'pending' },
386 | 						{ id: 2, title: 'Task 2', dependencies: [3], status: 'pending' },
387 | 						{ id: 3, title: 'Task 3', dependencies: [1], status: 'pending' } // Circular dependency
388 | 					]
389 | 				},
390 | 				'in-progress': {
391 | 					tasks: [
392 | 						{ id: 4, title: 'Task 4', dependencies: [], status: 'in-progress' }
393 | 					]
394 | 				}
395 | 			};
396 | 
397 | 			// Set up mock file system with circular dependency data
398 | 			mockFs({
399 | 				[testDataDir]: {
400 | 					'tasks.json': JSON.stringify(circularData, null, 2)
401 | 				}
402 | 			});
403 | 
404 | 			// Attempt to move Task 1 with dependencies should fail due to circular dependency
405 | 			await expect(
406 | 				moveTasksBetweenTags(
407 | 					testTasksPath,
408 | 					['1'],
409 | 					'backlog',
410 | 					'in-progress',
411 | 					{ withDependencies: true, ignoreDependencies: false },
412 | 					{ projectRoot: testDataDir }
413 | 				)
414 | 			).rejects.toThrow();
415 | 		});
416 | 
417 | 		it('should handle nested dependency chains using actual move functions', async () => {
418 | 			// Create data with nested dependency chains
419 | 			const nestedData = {
420 | 				backlog: {
421 | 					tasks: [
422 | 						{ id: 1, title: 'Task 1', dependencies: [2], status: 'pending' },
423 | 						{ id: 2, title: 'Task 2', dependencies: [3], status: 'pending' },
424 | 						{ id: 3, title: 'Task 3', dependencies: [4], status: 'pending' },
425 | 						{ id: 4, title: 'Task 4', dependencies: [], status: 'pending' }
426 | 					]
427 | 				},
428 | 				'in-progress': {
429 | 					tasks: [
430 | 						{ id: 5, title: 'Task 5', dependencies: [], status: 'in-progress' }
431 | 					]
432 | 				}
433 | 			};
434 | 
435 | 			// Set up mock file system with nested dependency data
436 | 			mockFs({
437 | 				[testDataDir]: {
438 | 					'tasks.json': JSON.stringify(nestedData, null, 2)
439 | 				}
440 | 			});
441 | 
442 | 			// Test moving Task 1 with all its nested dependencies
443 | 			const result = await moveTasksBetweenTags(
444 | 				testTasksPath,
445 | 				['1'], // Task 1
446 | 				'backlog',
447 | 				'in-progress',
448 | 				{ withDependencies: true, ignoreDependencies: false },
449 | 				{ projectRoot: testDataDir }
450 | 			);
451 | 
452 | 			// Verify the move operation was successful
453 | 			expect(result).toBeDefined();
454 | 			expect(result.message).toContain(
455 | 				'Successfully moved 4 tasks from "backlog" to "in-progress"'
456 | 			);
457 | 			expect(result.movedTasks).toHaveLength(4); // Tasks 1, 2, 3, 4
458 | 
459 | 			// Read the updated data to verify all tasks moved
460 | 			const updatedData = readJSON(testTasksPath, null, 'backlog');
461 | 			const rawData = updatedData._rawTaggedData || updatedData;
462 | 
463 | 			// Verify all tasks moved from backlog
464 | 			expect(rawData.backlog?.tasks || []).toHaveLength(0); // All tasks moved
465 | 
466 | 			// Verify all tasks are now in in-progress
467 | 			expect(rawData['in-progress']?.tasks || []).toHaveLength(5); // Task 5 + Tasks 1, 2, 3, 4
468 | 
469 | 			// Verify dependency relationships are preserved
470 | 			const task1 = rawData['in-progress']?.tasks?.find((t) => t.id === 1);
471 | 			const task2 = rawData['in-progress']?.tasks?.find((t) => t.id === 2);
472 | 			const task3 = rawData['in-progress']?.tasks?.find((t) => t.id === 3);
473 | 			const task4 = rawData['in-progress']?.tasks?.find((t) => t.id === 4);
474 | 
475 | 			expect(task1?.dependencies).toEqual([2]);
476 | 			expect(task2?.dependencies).toEqual([3]);
477 | 			expect(task3?.dependencies).toEqual([4]);
478 | 			expect(task4?.dependencies).toEqual([]);
479 | 		});
480 | 
481 | 		it('should handle cross-tag dependency resolution using actual move functions', async () => {
482 | 			// Create data with cross-tag dependencies
483 | 			const crossTagData = {
484 | 				backlog: {
485 | 					tasks: [
486 | 						{ id: 1, title: 'Task 1', dependencies: [5], status: 'pending' }, // Depends on task in in-progress
487 | 						{ id: 2, title: 'Task 2', dependencies: [], status: 'pending' }
488 | 					]
489 | 				},
490 | 				'in-progress': {
491 | 					tasks: [
492 | 						{ id: 5, title: 'Task 5', dependencies: [], status: 'in-progress' }
493 | 					]
494 | 				}
495 | 			};
496 | 
497 | 			// Set up mock file system with cross-tag dependency data
498 | 			mockFs({
499 | 				[testDataDir]: {
500 | 					'tasks.json': JSON.stringify(crossTagData, null, 2)
501 | 				}
502 | 			});
503 | 
504 | 			// Test moving Task 1 which depends on Task 5 in another tag
505 | 			const result = await moveTasksBetweenTags(
506 | 				testTasksPath,
507 | 				['1'], // Task 1
508 | 				'backlog',
509 | 				'in-progress',
510 | 				{ withDependencies: false, ignoreDependencies: false },
511 | 				{ projectRoot: testDataDir }
512 | 			);
513 | 
514 | 			// Verify the move operation was successful
515 | 			expect(result).toBeDefined();
516 | 			expect(result.message).toContain(
517 | 				'Successfully moved 1 tasks from "backlog" to "in-progress"'
518 | 			);
519 | 
520 | 			// Read the updated data to verify the move actually happened
521 | 			const updatedData = readJSON(testTasksPath, null, 'backlog');
522 | 			const rawData = updatedData._rawTaggedData || updatedData;
523 | 
524 | 			// Verify Task 1 is no longer in backlog
525 | 			const taskInBacklog = rawData.backlog?.tasks?.find((t) => t.id === 1);
526 | 			expect(taskInBacklog).toBeUndefined();
527 | 
528 | 			// Verify Task 1 is now in in-progress with its dependency preserved
529 | 			const taskInProgress = rawData['in-progress']?.tasks?.find(
530 | 				(t) => t.id === 1
531 | 			);
532 | 			expect(taskInProgress).toBeDefined();
533 | 			expect(taskInProgress.title).toBe('Task 1');
534 | 			expect(taskInProgress.dependencies).toEqual([5]); // Cross-tag dependency preserved
535 | 		});
536 | 	});
537 | });
538 | 
```

--------------------------------------------------------------------------------
/scripts/modules/task-manager/update-task-by-id.js:
--------------------------------------------------------------------------------

```javascript
  1 | import fs from 'fs';
  2 | import chalk from 'chalk';
  3 | import boxen from 'boxen';
  4 | import Table from 'cli-table3';
  5 | 
  6 | import {
  7 | 	readJSON,
  8 | 	writeJSON,
  9 | 	truncate,
 10 | 	flattenTasksWithSubtasks,
 11 | 	findProjectRoot
 12 | } from '../utils.js';
 13 | 
 14 | import {
 15 | 	getStatusWithColor,
 16 | 	startLoadingIndicator,
 17 | 	stopLoadingIndicator,
 18 | 	displayAiUsageSummary
 19 | } from '../ui.js';
 20 | 
 21 | import {
 22 | 	generateTextService,
 23 | 	generateObjectService
 24 | } from '../ai-services-unified.js';
 25 | import { COMMAND_SCHEMAS } from '../../../src/schemas/registry.js';
 26 | import {
 27 | 	isApiKeySet,
 28 | 	hasCodebaseAnalysis,
 29 | 	getDebugFlag
 30 | } from '../config-manager.js';
 31 | import { getPromptManager } from '../prompt-manager.js';
 32 | import { ContextGatherer } from '../utils/contextGatherer.js';
 33 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
 34 | import { tryUpdateViaRemote } from '@tm/bridge';
 35 | import { createBridgeLogger } from '../bridge-utils.js';
 36 | 
 37 | /**
 38 |  * Update a task by ID with new information using the unified AI service.
 39 |  * @param {string} tasksPath - Path to the tasks.json file
 40 |  * @param {string|number} taskId - ID of the task to update (supports numeric, alphanumeric like HAM-123, and subtask IDs like 1.2)
 41 |  * @param {string} prompt - Prompt for generating updated task information
 42 |  * @param {boolean} [useResearch=false] - Whether to use the research AI role.
 43 |  * @param {Object} context - Context object containing session and mcpLog.
 44 |  * @param {Object} [context.session] - Session object from MCP server.
 45 |  * @param {Object} [context.mcpLog] - MCP logger object.
 46 |  * @param {string} [context.projectRoot] - Project root path.
 47 |  * @param {string} [context.tag] - Tag for the task
 48 |  * @param {string} [outputFormat='text'] - Output format ('text' or 'json').
 49 |  * @param {boolean} [appendMode=false] - If true, append to details instead of full update.
 50 |  * @returns {Promise<Object|null>} - The updated task or null if update failed.
 51 |  */
 52 | async function updateTaskById(
 53 | 	tasksPath,
 54 | 	taskId,
 55 | 	prompt,
 56 | 	useResearch = false,
 57 | 	context = {},
 58 | 	outputFormat = 'text',
 59 | 	appendMode = false
 60 | ) {
 61 | 	const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
 62 | 	const { report, isMCP } = createBridgeLogger(mcpLog, session);
 63 | 
 64 | 	try {
 65 | 		report('info', `Updating single task ${taskId} with prompt: "${prompt}"`);
 66 | 
 67 | 		// --- Input Validations ---
 68 | 		// Note: taskId can be a number (1), string with dot (1.2), or display ID (HAM-123)
 69 | 		// So we don't validate it as strictly anymore
 70 | 		if (taskId === null || taskId === undefined || String(taskId).trim() === '')
 71 | 			throw new Error('Task ID cannot be empty.');
 72 | 
 73 | 		if (!prompt || typeof prompt !== 'string' || prompt.trim() === '')
 74 | 			throw new Error('Prompt cannot be empty.');
 75 | 
 76 | 		// Determine project root first (needed for API key checks)
 77 | 		const projectRoot = providedProjectRoot || findProjectRoot();
 78 | 		if (!projectRoot) {
 79 | 			throw new Error('Could not determine project root directory');
 80 | 		}
 81 | 
 82 | 		if (useResearch && !isApiKeySet('perplexity', session)) {
 83 | 			report(
 84 | 				'warn',
 85 | 				'Perplexity research requested but API key not set. Falling back.'
 86 | 			);
 87 | 			if (outputFormat === 'text')
 88 | 				console.log(
 89 | 					chalk.yellow('Perplexity AI not available. Falling back to main AI.')
 90 | 				);
 91 | 			useResearch = false;
 92 | 		}
 93 | 
 94 | 		// --- BRIDGE: Try remote update first (API storage) ---
 95 | 		const remoteResult = await tryUpdateViaRemote({
 96 | 			taskId,
 97 | 			prompt,
 98 | 			projectRoot,
 99 | 			tag,
100 | 			appendMode,
101 | 			useResearch,
102 | 			isMCP,
103 | 			outputFormat,
104 | 			report
105 | 		});
106 | 
107 | 		// If remote handled it, return the result
108 | 		if (remoteResult) {
109 | 			return remoteResult;
110 | 		}
111 | 		// Otherwise fall through to file-based logic below
112 | 		// --- End BRIDGE ---
113 | 
114 | 		// For file storage, ensure the tasks file exists
115 | 		if (!fs.existsSync(tasksPath))
116 | 			throw new Error(`Tasks file not found: ${tasksPath}`);
117 | 		// --- End Input Validations ---
118 | 
119 | 		// --- Task Loading and Status Check (Keep existing) ---
120 | 		const data = readJSON(tasksPath, projectRoot, tag);
121 | 		if (!data || !data.tasks)
122 | 			throw new Error(`No valid tasks found in ${tasksPath}.`);
123 | 		// File storage requires a strict numeric task ID
124 | 		const idStr = String(taskId).trim();
125 | 		if (!/^\d+$/.test(idStr)) {
126 | 			throw new Error(
127 | 				'For file storage, taskId must be a positive integer. ' +
128 | 					'Use update-subtask-by-id for IDs like "1.2", or run in API storage for display IDs (e.g., "HAM-123").'
129 | 			);
130 | 		}
131 | 		const numericTaskId = Number(idStr);
132 | 		const taskIndex = data.tasks.findIndex((task) => task.id === numericTaskId);
133 | 		if (taskIndex === -1) {
134 | 			report('error', `Task with ID ${numericTaskId} not found`);
135 | 			throw new Error(`Task with ID ${numericTaskId} not found.`);
136 | 		}
137 | 		const taskToUpdate = data.tasks[taskIndex];
138 | 		if (taskToUpdate.status === 'done' || taskToUpdate.status === 'completed') {
139 | 			report(
140 | 				'warn',
141 | 				`Task ${taskId} is already marked as done and cannot be updated`
142 | 			);
143 | 
144 | 			// Only show warning box for text output (CLI)
145 | 			if (outputFormat === 'text') {
146 | 				console.log(
147 | 					boxen(
148 | 						chalk.yellow(
149 | 							`Task ${taskId} is already marked as ${taskToUpdate.status} and cannot be updated.`
150 | 						) +
151 | 							'\n\n' +
152 | 							chalk.white(
153 | 								'Completed tasks are locked to maintain consistency. To modify a completed task, you must first:'
154 | 							) +
155 | 							'\n' +
156 | 							chalk.white(
157 | 								'1. Change its status to "pending" or "in-progress"'
158 | 							) +
159 | 							'\n' +
160 | 							chalk.white('2. Then run the update-task command'),
161 | 						{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
162 | 					)
163 | 				);
164 | 			}
165 | 			return null;
166 | 		}
167 | 		// --- End Task Loading ---
168 | 
169 | 		// --- Context Gathering ---
170 | 		let gatheredContext = '';
171 | 		try {
172 | 			const contextGatherer = new ContextGatherer(projectRoot, tag);
173 | 			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
174 | 			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-task');
175 | 			const searchQuery = `${taskToUpdate.title} ${taskToUpdate.description} ${prompt}`;
176 | 			const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
177 | 				maxResults: 5,
178 | 				includeSelf: true
179 | 			});
180 | 			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
181 | 
182 | 			const finalTaskIds = [
183 | 				...new Set([taskId.toString(), ...relevantTaskIds])
184 | 			];
185 | 
186 | 			if (finalTaskIds.length > 0) {
187 | 				const contextResult = await contextGatherer.gather({
188 | 					tasks: finalTaskIds,
189 | 					format: 'research'
190 | 				});
191 | 				gatheredContext = contextResult.context || '';
192 | 			}
193 | 		} catch (contextError) {
194 | 			report('warn', `Could not gather context: ${contextError.message}`);
195 | 		}
196 | 		// --- End Context Gathering ---
197 | 
198 | 		// --- Display Task Info (CLI Only - Keep existing) ---
199 | 		if (outputFormat === 'text') {
200 | 			// Show the task that will be updated
201 | 			const table = new Table({
202 | 				head: [
203 | 					chalk.cyan.bold('ID'),
204 | 					chalk.cyan.bold('Title'),
205 | 					chalk.cyan.bold('Status')
206 | 				],
207 | 				colWidths: [5, 60, 10]
208 | 			});
209 | 
210 | 			table.push([
211 | 				taskToUpdate.id,
212 | 				truncate(taskToUpdate.title, 57),
213 | 				getStatusWithColor(taskToUpdate.status)
214 | 			]);
215 | 
216 | 			console.log(
217 | 				boxen(chalk.white.bold(`Updating Task #${taskId}`), {
218 | 					padding: 1,
219 | 					borderColor: 'blue',
220 | 					borderStyle: 'round',
221 | 					margin: { top: 1, bottom: 0 }
222 | 				})
223 | 			);
224 | 
225 | 			console.log(table.toString());
226 | 
227 | 			// Display a message about how completed subtasks are handled
228 | 			console.log(
229 | 				boxen(
230 | 					chalk.cyan.bold('How Completed Subtasks Are Handled:') +
231 | 						'\n\n' +
232 | 						chalk.white(
233 | 							'• Subtasks marked as "done" or "completed" will be preserved\n'
234 | 						) +
235 | 						chalk.white(
236 | 							'• New subtasks will build upon what has already been completed\n'
237 | 						) +
238 | 						chalk.white(
239 | 							'• If completed work needs revision, a new subtask will be created instead of modifying done items\n'
240 | 						) +
241 | 						chalk.white(
242 | 							'• This approach maintains a clear record of completed work and new requirements'
243 | 						),
244 | 					{
245 | 						padding: 1,
246 | 						borderColor: 'blue',
247 | 						borderStyle: 'round',
248 | 						margin: { top: 1, bottom: 1 }
249 | 					}
250 | 				)
251 | 			);
252 | 		}
253 | 
254 | 		// --- Build Prompts using PromptManager ---
255 | 		const promptManager = getPromptManager();
256 | 
257 | 		const promptParams = {
258 | 			task: taskToUpdate,
259 | 			taskJson: JSON.stringify(taskToUpdate, null, 2),
260 | 			updatePrompt: prompt,
261 | 			appendMode: appendMode,
262 | 			useResearch: useResearch,
263 | 			currentDetails: taskToUpdate.details || '(No existing details)',
264 | 			gatheredContext: gatheredContext || '',
265 | 			hasCodebaseAnalysis: hasCodebaseAnalysis(
266 | 				useResearch,
267 | 				projectRoot,
268 | 				session
269 | 			),
270 | 			projectRoot: projectRoot
271 | 		};
272 | 
273 | 		const variantKey = appendMode
274 | 			? 'append'
275 | 			: useResearch
276 | 				? 'research'
277 | 				: 'default';
278 | 
279 | 		report(
280 | 			'info',
281 | 			`Loading prompt template with variant: ${variantKey}, appendMode: ${appendMode}, useResearch: ${useResearch}`
282 | 		);
283 | 
284 | 		let systemPrompt;
285 | 		let userPrompt;
286 | 		try {
287 | 			const promptResult = promptManager.loadPrompt(
288 | 				'update-task',
289 | 				promptParams,
290 | 				variantKey
291 | 			);
292 | 			report(
293 | 				'info',
294 | 				`Prompt result type: ${typeof promptResult}, keys: ${promptResult ? Object.keys(promptResult).join(', ') : 'null'}`
295 | 			);
296 | 
297 | 			// Extract prompts - loadPrompt returns { systemPrompt, userPrompt, metadata }
298 | 			systemPrompt = promptResult.systemPrompt;
299 | 			userPrompt = promptResult.userPrompt;
300 | 
301 | 			report(
302 | 				'info',
303 | 				`Loaded prompts - systemPrompt length: ${systemPrompt?.length}, userPrompt length: ${userPrompt?.length}`
304 | 			);
305 | 		} catch (error) {
306 | 			report('error', `Failed to load prompt template: ${error.message}`);
307 | 			throw new Error(`Failed to load prompt template: ${error.message}`);
308 | 		}
309 | 
310 | 		// If prompts are still not set, throw an error
311 | 		if (!systemPrompt || !userPrompt) {
312 | 			throw new Error(
313 | 				`Failed to load prompts: systemPrompt=${!!systemPrompt}, userPrompt=${!!userPrompt}`
314 | 			);
315 | 		}
316 | 		// --- End Build Prompts ---
317 | 
318 | 		let loadingIndicator = null;
319 | 		let aiServiceResponse = null;
320 | 
321 | 		if (!isMCP && outputFormat === 'text') {
322 | 			loadingIndicator = startLoadingIndicator(
323 | 				useResearch ? 'Updating task with research...\n' : 'Updating task...\n'
324 | 			);
325 | 		}
326 | 
327 | 		try {
328 | 			const serviceRole = useResearch ? 'research' : 'main';
329 | 
330 | 			if (appendMode) {
331 | 				// Append mode still uses generateTextService since it returns plain text
332 | 				aiServiceResponse = await generateTextService({
333 | 					role: serviceRole,
334 | 					session: session,
335 | 					projectRoot: projectRoot,
336 | 					systemPrompt: systemPrompt,
337 | 					prompt: userPrompt,
338 | 					commandName: 'update-task',
339 | 					outputType: isMCP ? 'mcp' : 'cli'
340 | 				});
341 | 			} else {
342 | 				// Full update mode uses generateObjectService for structured output
343 | 				aiServiceResponse = await generateObjectService({
344 | 					role: serviceRole,
345 | 					session: session,
346 | 					projectRoot: projectRoot,
347 | 					systemPrompt: systemPrompt,
348 | 					prompt: userPrompt,
349 | 					schema: COMMAND_SCHEMAS['update-task-by-id'],
350 | 					objectName: 'task',
351 | 					commandName: 'update-task',
352 | 					outputType: isMCP ? 'mcp' : 'cli'
353 | 				});
354 | 			}
355 | 
356 | 			if (loadingIndicator)
357 | 				stopLoadingIndicator(loadingIndicator, 'AI update complete.');
358 | 
359 | 			if (appendMode) {
360 | 				// Append mode: handle as plain text
361 | 				const generatedContentString = aiServiceResponse.mainResult;
362 | 				let newlyAddedSnippet = '';
363 | 
364 | 				if (generatedContentString && generatedContentString.trim()) {
365 | 					const timestamp = new Date().toISOString();
366 | 					const formattedBlock = `<info added on ${timestamp}>\n${generatedContentString.trim()}\n</info added on ${timestamp}>`;
367 | 					newlyAddedSnippet = formattedBlock;
368 | 
369 | 					// Append to task details
370 | 					taskToUpdate.details =
371 | 						(taskToUpdate.details ? taskToUpdate.details + '\n' : '') +
372 | 						formattedBlock;
373 | 				} else {
374 | 					report(
375 | 						'warn',
376 | 						'AI response was empty or whitespace after trimming. Original details remain unchanged.'
377 | 					);
378 | 					newlyAddedSnippet = 'No new details were added by the AI.';
379 | 				}
380 | 
381 | 				// Update description with timestamp if prompt is short
382 | 				if (prompt.length < 100) {
383 | 					if (taskToUpdate.description) {
384 | 						taskToUpdate.description += ` [Updated: ${new Date().toLocaleDateString()}]`;
385 | 					}
386 | 				}
387 | 
388 | 				// Write the updated task back to file
389 | 				data.tasks[taskIndex] = taskToUpdate;
390 | 				writeJSON(tasksPath, data, projectRoot, tag);
391 | 				report('success', `Successfully appended to task ${taskId}`);
392 | 
393 | 				// Display success message for CLI
394 | 				if (outputFormat === 'text') {
395 | 					console.log(
396 | 						boxen(
397 | 							chalk.green(`Successfully appended to task #${taskId}`) +
398 | 								'\n\n' +
399 | 								chalk.white.bold('Title:') +
400 | 								' ' +
401 | 								taskToUpdate.title +
402 | 								'\n\n' +
403 | 								chalk.white.bold('Newly Added Content:') +
404 | 								'\n' +
405 | 								chalk.white(newlyAddedSnippet),
406 | 							{ padding: 1, borderColor: 'green', borderStyle: 'round' }
407 | 						)
408 | 					);
409 | 				}
410 | 
411 | 				// Display AI usage telemetry for CLI users
412 | 				if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
413 | 					displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
414 | 				}
415 | 
416 | 				// Return the updated task
417 | 				return {
418 | 					updatedTask: taskToUpdate,
419 | 					telemetryData: aiServiceResponse.telemetryData,
420 | 					tagInfo: aiServiceResponse.tagInfo
421 | 				};
422 | 			}
423 | 
424 | 			// Full update mode: Use structured data directly
425 | 			const updatedTask = aiServiceResponse.mainResult.task;
426 | 
427 | 			// --- Task Validation/Correction (Keep existing logic) ---
428 | 			if (!updatedTask || typeof updatedTask !== 'object')
429 | 				throw new Error('Received invalid task object from AI.');
430 | 			if (!updatedTask.title || !updatedTask.description)
431 | 				throw new Error('Updated task missing required fields.');
432 | 			// Preserve ID if AI changed it
433 | 			if (updatedTask.id !== taskId) {
434 | 				report('warn', `AI changed task ID. Restoring original ID ${taskId}.`);
435 | 				updatedTask.id = taskId;
436 | 			}
437 | 			// Preserve status if AI changed it
438 | 			if (
439 | 				updatedTask.status !== taskToUpdate.status &&
440 | 				!prompt.toLowerCase().includes('status')
441 | 			) {
442 | 				report(
443 | 					'warn',
444 | 					`AI changed task status. Restoring original status '${taskToUpdate.status}'.`
445 | 				);
446 | 				updatedTask.status = taskToUpdate.status;
447 | 			}
448 | 			// Fix subtask IDs if they exist (ensure they are numeric and sequential)
449 | 			if (updatedTask.subtasks && Array.isArray(updatedTask.subtasks)) {
450 | 				let currentSubtaskId = 1;
451 | 				updatedTask.subtasks = updatedTask.subtasks.map((subtask) => {
452 | 					// Fix AI-generated subtask IDs that might be strings or use parent ID as prefix
453 | 					const correctedSubtask = {
454 | 						...subtask,
455 | 						id: currentSubtaskId, // Override AI-generated ID with correct sequential ID
456 | 						dependencies: Array.isArray(subtask.dependencies)
457 | 							? subtask.dependencies
458 | 									.map((dep) =>
459 | 										typeof dep === 'string' ? parseInt(dep, 10) : dep
460 | 									)
461 | 									.filter(
462 | 										(depId) =>
463 | 											!Number.isNaN(depId) &&
464 | 											depId >= 1 &&
465 | 											depId < currentSubtaskId
466 | 									)
467 | 							: [],
468 | 						status: subtask.status || 'pending'
469 | 					};
470 | 					currentSubtaskId++;
471 | 					return correctedSubtask;
472 | 				});
473 | 				report(
474 | 					'info',
475 | 					`Fixed ${updatedTask.subtasks.length} subtask IDs to be sequential numeric IDs.`
476 | 				);
477 | 			}
478 | 
479 | 			// Preserve completed subtasks (Keep existing logic)
480 | 			if (taskToUpdate.subtasks?.length > 0) {
481 | 				if (!updatedTask.subtasks) {
482 | 					report(
483 | 						'warn',
484 | 						'Subtasks removed by AI. Restoring original subtasks.'
485 | 					);
486 | 					updatedTask.subtasks = taskToUpdate.subtasks;
487 | 				} else {
488 | 					const completedOriginal = taskToUpdate.subtasks.filter(
489 | 						(st) => st.status === 'done' || st.status === 'completed'
490 | 					);
491 | 					completedOriginal.forEach((compSub) => {
492 | 						const updatedSub = updatedTask.subtasks.find(
493 | 							(st) => st.id === compSub.id
494 | 						);
495 | 						if (
496 | 							!updatedSub ||
497 | 							JSON.stringify(updatedSub) !== JSON.stringify(compSub)
498 | 						) {
499 | 							report(
500 | 								'warn',
501 | 								`Completed subtask ${compSub.id} was modified or removed. Restoring.`
502 | 							);
503 | 							// Remove potentially modified version
504 | 							updatedTask.subtasks = updatedTask.subtasks.filter(
505 | 								(st) => st.id !== compSub.id
506 | 							);
507 | 							// Add back original
508 | 							updatedTask.subtasks.push(compSub);
509 | 						}
510 | 					});
511 | 					// Deduplicate just in case
512 | 					const subtaskIds = new Set();
513 | 					updatedTask.subtasks = updatedTask.subtasks.filter((st) => {
514 | 						if (!subtaskIds.has(st.id)) {
515 | 							subtaskIds.add(st.id);
516 | 							return true;
517 | 						}
518 | 						report('warn', `Duplicate subtask ID ${st.id} removed.`);
519 | 						return false;
520 | 					});
521 | 				}
522 | 			}
523 | 			// --- End Task Validation/Correction ---
524 | 
525 | 			// --- Update Task Data (Keep existing) ---
526 | 			data.tasks[taskIndex] = updatedTask;
527 | 			// --- End Update Task Data ---
528 | 
529 | 			// --- Write File and Generate (Unchanged) ---
530 | 			writeJSON(tasksPath, data, projectRoot, tag);
531 | 			report('success', `Successfully updated task ${taskId}`);
532 | 			// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
533 | 			// --- End Write File ---
534 | 
535 | 			// --- Display CLI Telemetry ---
536 | 			if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
537 | 				displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); // <<< ADD display
538 | 			}
539 | 
540 | 			// --- Return Success with Telemetry ---
541 | 			return {
542 | 				updatedTask: updatedTask, // Return the updated task object
543 | 				telemetryData: aiServiceResponse.telemetryData, // <<< ADD telemetryData
544 | 				tagInfo: aiServiceResponse.tagInfo
545 | 			};
546 | 		} catch (error) {
547 | 			// Catch errors from generateTextService
548 | 			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
549 | 			report('error', `Error during AI service call: ${error.message}`);
550 | 			if (error.message.includes('API key')) {
551 | 				report('error', 'Please ensure API keys are configured correctly.');
552 | 			}
553 | 			throw error; // Re-throw error
554 | 		}
555 | 	} catch (error) {
556 | 		// General error catch
557 | 		// --- General Error Handling (Keep existing) ---
558 | 		report('error', `Error updating task: ${error.message}`);
559 | 		if (outputFormat === 'text') {
560 | 			console.error(chalk.red(`Error: ${error.message}`));
561 | 			// ... helpful hints ...
562 | 			if (getDebugFlag(session)) console.error(error);
563 | 			process.exit(1);
564 | 		}
565 | 		throw error; // Re-throw for MCP
566 | 		// --- End General Error Handling ---
567 | 	}
568 | }
569 | 
570 | export default updateTaskById;
571 | 
```
Page 42/69FirstPrevNextLast