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

# Directory Structure

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

# Files

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

```javascript
   1 | /**
   2 |  * Tests for the expand-task.js module
   3 |  */
   4 | import { jest } from '@jest/globals';
   5 | import fs from 'fs';
   6 | import {
   7 | 	createGetTagAwareFilePathMock,
   8 | 	createSlugifyTagForFilePathMock
   9 | } from './setup.js';
  10 | 
  11 | // Mock the dependencies before importing the module under test
  12 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
  13 | 	readJSON: jest.fn(),
  14 | 	writeJSON: jest.fn(),
  15 | 	log: jest.fn(),
  16 | 	CONFIG: {
  17 | 		model: 'mock-claude-model',
  18 | 		maxTokens: 4000,
  19 | 		temperature: 0.7,
  20 | 		debug: false
  21 | 	},
  22 | 	sanitizePrompt: jest.fn((prompt) => prompt),
  23 | 	truncate: jest.fn((text) => text),
  24 | 	isSilentMode: jest.fn(() => false),
  25 | 	findTaskById: jest.fn(),
  26 | 	findProjectRoot: jest.fn((tasksPath) => '/mock/project/root'),
  27 | 	getCurrentTag: jest.fn(() => 'master'),
  28 | 	ensureTagMetadata: jest.fn((tagObj) => tagObj),
  29 | 	flattenTasksWithSubtasks: jest.fn((tasks) => {
  30 | 		const allTasks = [];
  31 | 		const queue = [...(tasks || [])];
  32 | 		while (queue.length > 0) {
  33 | 			const task = queue.shift();
  34 | 			allTasks.push(task);
  35 | 			if (task.subtasks) {
  36 | 				for (const subtask of task.subtasks) {
  37 | 					queue.push({ ...subtask, id: `${task.id}.${subtask.id}` });
  38 | 				}
  39 | 			}
  40 | 		}
  41 | 		return allTasks;
  42 | 	}),
  43 | 	getTagAwareFilePath: createGetTagAwareFilePathMock(),
  44 | 	slugifyTagForFilePath: createSlugifyTagForFilePathMock(),
  45 | 	readComplexityReport: jest.fn(),
  46 | 	markMigrationForNotice: jest.fn(),
  47 | 	performCompleteTagMigration: jest.fn(),
  48 | 	setTasksForTag: jest.fn(),
  49 | 	getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || [])
  50 | }));
  51 | 
  52 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
  53 | 	displayBanner: jest.fn(),
  54 | 	getStatusWithColor: jest.fn((status) => status),
  55 | 	startLoadingIndicator: jest.fn(),
  56 | 	stopLoadingIndicator: jest.fn(),
  57 | 	succeedLoadingIndicator: jest.fn(),
  58 | 	failLoadingIndicator: jest.fn(),
  59 | 	warnLoadingIndicator: jest.fn(),
  60 | 	infoLoadingIndicator: jest.fn(),
  61 | 	displayAiUsageSummary: jest.fn(),
  62 | 	displayContextAnalysis: jest.fn()
  63 | }));
  64 | 
  65 | jest.unstable_mockModule(
  66 | 	'../../../../../scripts/modules/ai-services-unified.js',
  67 | 	() => ({
  68 | 		generateTextService: jest.fn().mockResolvedValue({
  69 | 			mainResult: JSON.stringify({
  70 | 				subtasks: [
  71 | 					{
  72 | 						id: 1,
  73 | 						title: 'Set up project structure',
  74 | 						description:
  75 | 							'Create the basic project directory structure and configuration files',
  76 | 						dependencies: [],
  77 | 						details:
  78 | 							'Initialize package.json, create src/ and test/ directories, set up linting configuration',
  79 | 						status: 'pending',
  80 | 						testStrategy:
  81 | 							'Verify all expected files and directories are created'
  82 | 					},
  83 | 					{
  84 | 						id: 2,
  85 | 						title: 'Implement core functionality',
  86 | 						description: 'Develop the main application logic and core features',
  87 | 						dependencies: [1],
  88 | 						details:
  89 | 							'Create main classes, implement business logic, set up data models',
  90 | 						status: 'pending',
  91 | 						testStrategy: 'Unit tests for all core functions and classes'
  92 | 					},
  93 | 					{
  94 | 						id: 3,
  95 | 						title: 'Add user interface',
  96 | 						description: 'Create the user interface components and layouts',
  97 | 						dependencies: [2],
  98 | 						details:
  99 | 							'Design UI components, implement responsive layouts, add user interactions',
 100 | 						status: 'pending',
 101 | 						testStrategy: 'UI tests and visual regression testing'
 102 | 					}
 103 | 				]
 104 | 			}),
 105 | 			telemetryData: {
 106 | 				timestamp: new Date().toISOString(),
 107 | 				userId: '1234567890',
 108 | 				commandName: 'expand-task',
 109 | 				modelUsed: 'claude-3-5-sonnet',
 110 | 				providerName: 'anthropic',
 111 | 				inputTokens: 1000,
 112 | 				outputTokens: 500,
 113 | 				totalTokens: 1500,
 114 | 				totalCost: 0.012414,
 115 | 				currency: 'USD'
 116 | 			}
 117 | 		})
 118 | 	})
 119 | );
 120 | 
 121 | jest.unstable_mockModule(
 122 | 	'../../../../../scripts/modules/config-manager.js',
 123 | 	() => ({
 124 | 		getDefaultSubtasks: jest.fn(() => 3),
 125 | 		getDebugFlag: jest.fn(() => false),
 126 | 		getDefaultNumTasks: jest.fn(() => 10),
 127 | 		getMainProvider: jest.fn(() => 'openai'),
 128 | 		getResearchProvider: jest.fn(() => 'perplexity'),
 129 | 		hasCodebaseAnalysis: jest.fn(() => false)
 130 | 	})
 131 | );
 132 | 
 133 | jest.unstable_mockModule(
 134 | 	'../../../../../scripts/modules/utils/contextGatherer.js',
 135 | 	() => ({
 136 | 		ContextGatherer: jest.fn().mockImplementation(() => ({
 137 | 			gather: jest.fn().mockResolvedValue({
 138 | 				context: 'Mock project context from files'
 139 | 			})
 140 | 		}))
 141 | 	})
 142 | );
 143 | 
 144 | jest.unstable_mockModule(
 145 | 	'../../../../../scripts/modules/utils/fuzzyTaskSearch.js',
 146 | 	() => ({
 147 | 		FuzzyTaskSearch: jest.fn().mockImplementation(() => ({
 148 | 			findRelevantTasks: jest.fn().mockReturnValue([]),
 149 | 			getTaskIds: jest.fn().mockReturnValue([])
 150 | 		}))
 151 | 	})
 152 | );
 153 | 
 154 | jest.unstable_mockModule(
 155 | 	'../../../../../scripts/modules/task-manager/generate-task-files.js',
 156 | 	() => ({
 157 | 		default: jest.fn().mockResolvedValue()
 158 | 	})
 159 | );
 160 | 
 161 | jest.unstable_mockModule(
 162 | 	'../../../../../scripts/modules/prompt-manager.js',
 163 | 	() => ({
 164 | 		getPromptManager: jest.fn().mockReturnValue({
 165 | 			loadPrompt: jest.fn().mockResolvedValue({
 166 | 				systemPrompt: 'Mocked system prompt',
 167 | 				userPrompt: 'Mocked user prompt'
 168 | 			})
 169 | 		})
 170 | 	})
 171 | );
 172 | 
 173 | // Mock external UI libraries
 174 | jest.unstable_mockModule('chalk', () => ({
 175 | 	default: {
 176 | 		white: { bold: jest.fn((text) => text) },
 177 | 		cyan: Object.assign(
 178 | 			jest.fn((text) => text),
 179 | 			{
 180 | 				bold: jest.fn((text) => text)
 181 | 			}
 182 | 		),
 183 | 		green: jest.fn((text) => text),
 184 | 		yellow: jest.fn((text) => text),
 185 | 		bold: jest.fn((text) => text)
 186 | 	}
 187 | }));
 188 | 
 189 | jest.unstable_mockModule('boxen', () => ({
 190 | 	default: jest.fn((text) => text)
 191 | }));
 192 | 
 193 | jest.unstable_mockModule('cli-table3', () => ({
 194 | 	default: jest.fn().mockImplementation(() => ({
 195 | 		push: jest.fn(),
 196 | 		toString: jest.fn(() => 'mocked table')
 197 | 	}))
 198 | }));
 199 | 
 200 | // Mock process.exit to prevent Jest worker crashes
 201 | const mockExit = jest.spyOn(process, 'exit').mockImplementation((code) => {
 202 | 	throw new Error(`process.exit called with "${code}"`);
 203 | });
 204 | 
 205 | // Import the mocked modules
 206 | const {
 207 | 	readJSON,
 208 | 	writeJSON,
 209 | 	log,
 210 | 	findTaskById,
 211 | 	ensureTagMetadata,
 212 | 	readComplexityReport,
 213 | 	findProjectRoot
 214 | } = await import('../../../../../scripts/modules/utils.js');
 215 | 
 216 | const { generateTextService } = await import(
 217 | 	'../../../../../scripts/modules/ai-services-unified.js'
 218 | );
 219 | 
 220 | const generateTaskFiles = (
 221 | 	await import(
 222 | 		'../../../../../scripts/modules/task-manager/generate-task-files.js'
 223 | 	)
 224 | ).default;
 225 | 
 226 | const { getDefaultSubtasks } = await import(
 227 | 	'../../../../../scripts/modules/config-manager.js'
 228 | );
 229 | 
 230 | // Import the module under test
 231 | const { default: expandTask } = await import(
 232 | 	'../../../../../scripts/modules/task-manager/expand-task.js'
 233 | );
 234 | 
 235 | describe('expandTask', () => {
 236 | 	const sampleTasks = {
 237 | 		master: {
 238 | 			tasks: [
 239 | 				{
 240 | 					id: 1,
 241 | 					title: 'Task 1',
 242 | 					description: 'First task',
 243 | 					status: 'done',
 244 | 					dependencies: [],
 245 | 					details: 'Already completed task',
 246 | 					subtasks: []
 247 | 				},
 248 | 				{
 249 | 					id: 2,
 250 | 					title: 'Task 2',
 251 | 					description: 'Second task',
 252 | 					status: 'pending',
 253 | 					dependencies: [],
 254 | 					details: 'Task ready for expansion',
 255 | 					subtasks: []
 256 | 				},
 257 | 				{
 258 | 					id: 3,
 259 | 					title: 'Complex Task',
 260 | 					description: 'A complex task that needs breakdown',
 261 | 					status: 'pending',
 262 | 					dependencies: [1],
 263 | 					details: 'This task involves multiple steps',
 264 | 					subtasks: []
 265 | 				},
 266 | 				{
 267 | 					id: 4,
 268 | 					title: 'Task with existing subtasks',
 269 | 					description: 'Task that already has subtasks',
 270 | 					status: 'pending',
 271 | 					dependencies: [],
 272 | 					details: 'Has existing subtasks',
 273 | 					subtasks: [
 274 | 						{
 275 | 							id: 1,
 276 | 							title: 'Existing subtask',
 277 | 							description: 'Already exists',
 278 | 							status: 'pending',
 279 | 							dependencies: []
 280 | 						}
 281 | 					]
 282 | 				}
 283 | 			]
 284 | 		},
 285 | 		'feature-branch': {
 286 | 			tasks: [
 287 | 				{
 288 | 					id: 1,
 289 | 					title: 'Feature Task 1',
 290 | 					description: 'Task in feature branch',
 291 | 					status: 'pending',
 292 | 					dependencies: [],
 293 | 					details: 'Feature-specific task',
 294 | 					subtasks: []
 295 | 				}
 296 | 			]
 297 | 		}
 298 | 	};
 299 | 
 300 | 	// Create a helper function for consistent mcpLog mock
 301 | 	const createMcpLogMock = () => ({
 302 | 		info: jest.fn(),
 303 | 		warn: jest.fn(),
 304 | 		error: jest.fn(),
 305 | 		debug: jest.fn(),
 306 | 		success: jest.fn()
 307 | 	});
 308 | 
 309 | 	beforeEach(() => {
 310 | 		jest.clearAllMocks();
 311 | 		mockExit.mockClear();
 312 | 
 313 | 		// Default readJSON implementation - returns tagged structure
 314 | 		readJSON.mockImplementation((tasksPath, projectRoot, tag) => {
 315 | 			const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
 316 | 			const selectedTag = tag || 'master';
 317 | 			return {
 318 | 				...sampleTasksCopy[selectedTag],
 319 | 				tag: selectedTag,
 320 | 				_rawTaggedData: sampleTasksCopy
 321 | 			};
 322 | 		});
 323 | 
 324 | 		// Default findTaskById implementation
 325 | 		findTaskById.mockImplementation((tasks, taskId) => {
 326 | 			const id = parseInt(taskId, 10);
 327 | 			return tasks.find((t) => t.id === id);
 328 | 		});
 329 | 
 330 | 		// Default complexity report (no report available)
 331 | 		readComplexityReport.mockReturnValue(null);
 332 | 
 333 | 		// Mock findProjectRoot to return consistent path for complexity report
 334 | 		findProjectRoot.mockReturnValue('/mock/project/root');
 335 | 
 336 | 		writeJSON.mockResolvedValue();
 337 | 		generateTaskFiles.mockResolvedValue();
 338 | 		log.mockImplementation(() => {});
 339 | 
 340 | 		// Mock console.log to avoid output during tests
 341 | 		jest.spyOn(console, 'log').mockImplementation(() => {});
 342 | 	});
 343 | 
 344 | 	afterEach(() => {
 345 | 		console.log.mockRestore();
 346 | 	});
 347 | 
 348 | 	describe('Basic Functionality', () => {
 349 | 		test('should expand a task with AI-generated subtasks', async () => {
 350 | 			// Arrange
 351 | 			const tasksPath = 'tasks/tasks.json';
 352 | 			const taskId = '2';
 353 | 			const numSubtasks = 3;
 354 | 			const context = {
 355 | 				mcpLog: createMcpLogMock(),
 356 | 				projectRoot: '/mock/project/root'
 357 | 			};
 358 | 
 359 | 			// Act
 360 | 			const result = await expandTask(
 361 | 				tasksPath,
 362 | 				taskId,
 363 | 				numSubtasks,
 364 | 				false,
 365 | 				'',
 366 | 				context,
 367 | 				false
 368 | 			);
 369 | 
 370 | 			// Assert
 371 | 			expect(readJSON).toHaveBeenCalledWith(
 372 | 				tasksPath,
 373 | 				'/mock/project/root',
 374 | 				undefined
 375 | 			);
 376 | 			expect(generateTextService).toHaveBeenCalledWith(expect.any(Object));
 377 | 			expect(writeJSON).toHaveBeenCalledWith(
 378 | 				tasksPath,
 379 | 				expect.objectContaining({
 380 | 					tasks: expect.arrayContaining([
 381 | 						expect.objectContaining({
 382 | 							id: 2,
 383 | 							subtasks: expect.arrayContaining([
 384 | 								expect.objectContaining({
 385 | 									id: 1,
 386 | 									title: 'Set up project structure',
 387 | 									status: 'pending'
 388 | 								}),
 389 | 								expect.objectContaining({
 390 | 									id: 2,
 391 | 									title: 'Implement core functionality',
 392 | 									status: 'pending'
 393 | 								}),
 394 | 								expect.objectContaining({
 395 | 									id: 3,
 396 | 									title: 'Add user interface',
 397 | 									status: 'pending'
 398 | 								})
 399 | 							])
 400 | 						})
 401 | 					]),
 402 | 					tag: 'master',
 403 | 					_rawTaggedData: expect.objectContaining({
 404 | 						master: expect.objectContaining({
 405 | 							tasks: expect.any(Array)
 406 | 						})
 407 | 					})
 408 | 				}),
 409 | 				'/mock/project/root',
 410 | 				undefined
 411 | 			);
 412 | 			expect(result).toEqual(
 413 | 				expect.objectContaining({
 414 | 					task: expect.objectContaining({
 415 | 						id: 2,
 416 | 						subtasks: expect.arrayContaining([
 417 | 							expect.objectContaining({
 418 | 								id: 1,
 419 | 								title: 'Set up project structure',
 420 | 								status: 'pending'
 421 | 							}),
 422 | 							expect.objectContaining({
 423 | 								id: 2,
 424 | 								title: 'Implement core functionality',
 425 | 								status: 'pending'
 426 | 							}),
 427 | 							expect.objectContaining({
 428 | 								id: 3,
 429 | 								title: 'Add user interface',
 430 | 								status: 'pending'
 431 | 							})
 432 | 						])
 433 | 					}),
 434 | 					telemetryData: expect.any(Object)
 435 | 				})
 436 | 			);
 437 | 		});
 438 | 
 439 | 		test('should handle research flag correctly', async () => {
 440 | 			// Arrange
 441 | 			const tasksPath = 'tasks/tasks.json';
 442 | 			const taskId = '2';
 443 | 			const numSubtasks = 3;
 444 | 			const context = {
 445 | 				mcpLog: createMcpLogMock(),
 446 | 				projectRoot: '/mock/project/root'
 447 | 			};
 448 | 
 449 | 			// Act
 450 | 			await expandTask(
 451 | 				tasksPath,
 452 | 				taskId,
 453 | 				numSubtasks,
 454 | 				true, // useResearch = true
 455 | 				'Additional context for research',
 456 | 				context,
 457 | 				false
 458 | 			);
 459 | 
 460 | 			// Assert
 461 | 			expect(generateTextService).toHaveBeenCalledWith(
 462 | 				expect.objectContaining({
 463 | 					role: 'research',
 464 | 					commandName: expect.any(String)
 465 | 				})
 466 | 			);
 467 | 		});
 468 | 
 469 | 		test('should handle complexity report integration without errors', async () => {
 470 | 			// Arrange
 471 | 			const tasksPath = 'tasks/tasks.json';
 472 | 			const taskId = '2';
 473 | 			const context = {
 474 | 				mcpLog: createMcpLogMock(),
 475 | 				projectRoot: '/mock/project/root'
 476 | 			};
 477 | 
 478 | 			// Act & Assert - Should complete without errors
 479 | 			const result = await expandTask(
 480 | 				tasksPath,
 481 | 				taskId,
 482 | 				undefined, // numSubtasks not specified
 483 | 				false,
 484 | 				'',
 485 | 				context,
 486 | 				false
 487 | 			);
 488 | 
 489 | 			// Assert - Should successfully expand and return expected structure
 490 | 			expect(result).toEqual(
 491 | 				expect.objectContaining({
 492 | 					task: expect.objectContaining({
 493 | 						id: 2,
 494 | 						subtasks: expect.any(Array)
 495 | 					}),
 496 | 					telemetryData: expect.any(Object)
 497 | 				})
 498 | 			);
 499 | 			expect(generateTextService).toHaveBeenCalled();
 500 | 		});
 501 | 	});
 502 | 
 503 | 	describe('Tag Handling (The Critical Bug Fix)', () => {
 504 | 		test('should preserve tagged structure when expanding with default tag', async () => {
 505 | 			// Arrange
 506 | 			const tasksPath = 'tasks/tasks.json';
 507 | 			const taskId = '2';
 508 | 			const context = {
 509 | 				mcpLog: createMcpLogMock(),
 510 | 				projectRoot: '/mock/project/root',
 511 | 				tag: 'master' // Explicit tag context
 512 | 			};
 513 | 
 514 | 			// Act
 515 | 			await expandTask(tasksPath, taskId, 3, false, '', context, false);
 516 | 
 517 | 			// Assert - CRITICAL: Check tag is passed to readJSON and writeJSON
 518 | 			expect(readJSON).toHaveBeenCalledWith(
 519 | 				tasksPath,
 520 | 				'/mock/project/root',
 521 | 				'master'
 522 | 			);
 523 | 			expect(writeJSON).toHaveBeenCalledWith(
 524 | 				tasksPath,
 525 | 				expect.objectContaining({
 526 | 					tag: 'master',
 527 | 					_rawTaggedData: expect.objectContaining({
 528 | 						master: expect.any(Object),
 529 | 						'feature-branch': expect.any(Object)
 530 | 					})
 531 | 				}),
 532 | 				'/mock/project/root',
 533 | 				'master' // CRITICAL: Tag must be passed to writeJSON
 534 | 			);
 535 | 		});
 536 | 
 537 | 		test('should preserve tagged structure when expanding with non-default tag', async () => {
 538 | 			// Arrange
 539 | 			const tasksPath = 'tasks/tasks.json';
 540 | 			const taskId = '1'; // Task in feature-branch
 541 | 			const context = {
 542 | 				mcpLog: createMcpLogMock(),
 543 | 				projectRoot: '/mock/project/root',
 544 | 				tag: 'feature-branch' // Different tag context
 545 | 			};
 546 | 
 547 | 			// Configure readJSON to return feature-branch data
 548 | 			readJSON.mockImplementation((tasksPath, projectRoot, tag) => {
 549 | 				const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
 550 | 				return {
 551 | 					...sampleTasksCopy['feature-branch'],
 552 | 					tag: 'feature-branch',
 553 | 					_rawTaggedData: sampleTasksCopy
 554 | 				};
 555 | 			});
 556 | 
 557 | 			// Act
 558 | 			await expandTask(tasksPath, taskId, 3, false, '', context, false);
 559 | 
 560 | 			// Assert - CRITICAL: Check tag preservation for non-default tag
 561 | 			expect(readJSON).toHaveBeenCalledWith(
 562 | 				tasksPath,
 563 | 				'/mock/project/root',
 564 | 				'feature-branch'
 565 | 			);
 566 | 			expect(writeJSON).toHaveBeenCalledWith(
 567 | 				tasksPath,
 568 | 				expect.objectContaining({
 569 | 					tag: 'feature-branch',
 570 | 					_rawTaggedData: expect.objectContaining({
 571 | 						master: expect.any(Object),
 572 | 						'feature-branch': expect.any(Object)
 573 | 					})
 574 | 				}),
 575 | 				'/mock/project/root',
 576 | 				'feature-branch' // CRITICAL: Correct tag passed to writeJSON
 577 | 			);
 578 | 		});
 579 | 
 580 | 		test('should NOT corrupt tagged structure when tag is undefined', async () => {
 581 | 			// Arrange
 582 | 			const tasksPath = 'tasks/tasks.json';
 583 | 			const taskId = '2';
 584 | 			const context = {
 585 | 				mcpLog: createMcpLogMock(),
 586 | 				projectRoot: '/mock/project/root'
 587 | 				// No tag specified - should default gracefully
 588 | 			};
 589 | 
 590 | 			// Act
 591 | 			await expandTask(tasksPath, taskId, 3, false, '', context, false);
 592 | 
 593 | 			// Assert - Should still preserve structure with undefined tag
 594 | 			expect(readJSON).toHaveBeenCalledWith(
 595 | 				tasksPath,
 596 | 				'/mock/project/root',
 597 | 				undefined
 598 | 			);
 599 | 			expect(writeJSON).toHaveBeenCalledWith(
 600 | 				tasksPath,
 601 | 				expect.objectContaining({
 602 | 					_rawTaggedData: expect.objectContaining({
 603 | 						master: expect.any(Object)
 604 | 					})
 605 | 				}),
 606 | 				'/mock/project/root',
 607 | 				undefined
 608 | 			);
 609 | 
 610 | 			// CRITICAL: Verify structure is NOT flattened to old format
 611 | 			const writeCallArgs = writeJSON.mock.calls[0][1];
 612 | 			expect(writeCallArgs).toHaveProperty('tasks'); // Should have tasks property from readJSON mock
 613 | 			expect(writeCallArgs).toHaveProperty('_rawTaggedData'); // Should preserve tagged structure
 614 | 		});
 615 | 	});
 616 | 
 617 | 	describe('Force Flag Handling', () => {
 618 | 		test('should replace existing subtasks when force=true', async () => {
 619 | 			// Arrange
 620 | 			const tasksPath = 'tasks/tasks.json';
 621 | 			const taskId = '4'; // Task with existing subtasks
 622 | 			const context = {
 623 | 				mcpLog: createMcpLogMock(),
 624 | 				projectRoot: '/mock/project/root'
 625 | 			};
 626 | 
 627 | 			// Act
 628 | 			await expandTask(tasksPath, taskId, 3, false, '', context, true);
 629 | 
 630 | 			// Assert - Should replace existing subtasks
 631 | 			expect(writeJSON).toHaveBeenCalledWith(
 632 | 				tasksPath,
 633 | 				expect.objectContaining({
 634 | 					tasks: expect.arrayContaining([
 635 | 						expect.objectContaining({
 636 | 							id: 4,
 637 | 							subtasks: expect.arrayContaining([
 638 | 								expect.objectContaining({
 639 | 									id: 1,
 640 | 									title: 'Set up project structure'
 641 | 								})
 642 | 							])
 643 | 						})
 644 | 					])
 645 | 				}),
 646 | 				'/mock/project/root',
 647 | 				undefined
 648 | 			);
 649 | 		});
 650 | 
 651 | 		test('should append to existing subtasks when force=false', async () => {
 652 | 			// Arrange
 653 | 			const tasksPath = 'tasks/tasks.json';
 654 | 			const taskId = '4'; // Task with existing subtasks
 655 | 			const context = {
 656 | 				mcpLog: createMcpLogMock(),
 657 | 				projectRoot: '/mock/project/root'
 658 | 			};
 659 | 
 660 | 			// Act
 661 | 			await expandTask(tasksPath, taskId, 3, false, '', context, false);
 662 | 
 663 | 			// Assert - Should append to existing subtasks with proper ID increments
 664 | 			expect(writeJSON).toHaveBeenCalledWith(
 665 | 				tasksPath,
 666 | 				expect.objectContaining({
 667 | 					tasks: expect.arrayContaining([
 668 | 						expect.objectContaining({
 669 | 							id: 4,
 670 | 							subtasks: expect.arrayContaining([
 671 | 								// Should contain both existing and new subtasks
 672 | 								expect.any(Object),
 673 | 								expect.any(Object),
 674 | 								expect.any(Object),
 675 | 								expect.any(Object) // 1 existing + 3 new = 4 total
 676 | 							])
 677 | 						})
 678 | 					])
 679 | 				}),
 680 | 				'/mock/project/root',
 681 | 				undefined
 682 | 			);
 683 | 		});
 684 | 	});
 685 | 
 686 | 	describe('Complexity Report Integration (Tag-Specific)', () => {
 687 | 		test('should use tag-specific complexity report when available', async () => {
 688 | 			// Arrange
 689 | 			const { getPromptManager } = await import(
 690 | 				'../../../../../scripts/modules/prompt-manager.js'
 691 | 			);
 692 | 			const mockLoadPrompt = jest.fn().mockResolvedValue({
 693 | 				systemPrompt: 'Generate exactly 5 subtasks for complexity report',
 694 | 				userPrompt:
 695 | 					'Please break this task into 5 parts\n\nUser provided context'
 696 | 			});
 697 | 			getPromptManager.mockReturnValue({
 698 | 				loadPrompt: mockLoadPrompt
 699 | 			});
 700 | 
 701 | 			const tasksPath = 'tasks/tasks.json';
 702 | 			const taskId = '1'; // Task in feature-branch
 703 | 			const context = {
 704 | 				mcpLog: createMcpLogMock(),
 705 | 				projectRoot: '/mock/project/root',
 706 | 				tag: 'feature-branch',
 707 | 				complexityReportPath:
 708 | 					'/mock/project/root/task-complexity-report_feature-branch.json'
 709 | 			};
 710 | 
 711 | 			// Stub fs.existsSync to simulate complexity report exists for this tag
 712 | 			const existsSpy = jest
 713 | 				.spyOn(fs, 'existsSync')
 714 | 				.mockImplementation((filepath) =>
 715 | 					filepath.endsWith('task-complexity-report_feature-branch.json')
 716 | 				);
 717 | 
 718 | 			// Stub readJSON to return complexity report when reading the report path
 719 | 			readJSON.mockImplementation((filepath, projectRootParam, tagParam) => {
 720 | 				if (filepath.includes('task-complexity-report_feature-branch.json')) {
 721 | 					return {
 722 | 						complexityAnalysis: [
 723 | 							{
 724 | 								taskId: 1,
 725 | 								complexityScore: 8,
 726 | 								recommendedSubtasks: 5,
 727 | 								reasoning: 'Needs five detailed steps',
 728 | 								expansionPrompt: 'Please break this task into 5 parts'
 729 | 							}
 730 | 						]
 731 | 					};
 732 | 				}
 733 | 				// Default tasks data for tasks.json
 734 | 				const sampleTasksCopy = JSON.parse(JSON.stringify(sampleTasks));
 735 | 				const selectedTag = tagParam || 'master';
 736 | 				return {
 737 | 					...sampleTasksCopy[selectedTag],
 738 | 					tag: selectedTag,
 739 | 					_rawTaggedData: sampleTasksCopy
 740 | 				};
 741 | 			});
 742 | 
 743 | 			// Act
 744 | 			await expandTask(tasksPath, taskId, undefined, false, '', context, false);
 745 | 
 746 | 			// Assert - generateTextService called with systemPrompt for 5 subtasks
 747 | 			const callArg = generateTextService.mock.calls[0][0];
 748 | 			expect(callArg.systemPrompt).toContain('Generate exactly 5 subtasks');
 749 | 
 750 | 			// Assert - Should use complexity-report variant with expansion prompt
 751 | 			expect(mockLoadPrompt).toHaveBeenCalledWith(
 752 | 				'expand-task',
 753 | 				expect.objectContaining({
 754 | 					subtaskCount: 5,
 755 | 					expansionPrompt: 'Please break this task into 5 parts'
 756 | 				}),
 757 | 				'complexity-report'
 758 | 			);
 759 | 
 760 | 			// Clean up stub
 761 | 			existsSpy.mockRestore();
 762 | 		});
 763 | 	});
 764 | 
 765 | 	describe('Error Handling', () => {
 766 | 		test('should handle non-existent task ID', async () => {
 767 | 			// Arrange
 768 | 			const tasksPath = 'tasks/tasks.json';
 769 | 			const taskId = '999'; // Non-existent task
 770 | 			const context = {
 771 | 				mcpLog: createMcpLogMock(),
 772 | 				projectRoot: '/mock/project/root'
 773 | 			};
 774 | 
 775 | 			findTaskById.mockReturnValue(null);
 776 | 
 777 | 			// Act & Assert
 778 | 			await expect(
 779 | 				expandTask(tasksPath, taskId, 3, false, '', context, false)
 780 | 			).rejects.toThrow('Task 999 not found');
 781 | 
 782 | 			expect(writeJSON).not.toHaveBeenCalled();
 783 | 		});
 784 | 
 785 | 		test('should expand tasks regardless of status (including done tasks)', async () => {
 786 | 			// Arrange
 787 | 			const tasksPath = 'tasks/tasks.json';
 788 | 			const taskId = '1'; // Task with 'done' status
 789 | 			const context = {
 790 | 				mcpLog: createMcpLogMock(),
 791 | 				projectRoot: '/mock/project/root'
 792 | 			};
 793 | 
 794 | 			// Act
 795 | 			const result = await expandTask(
 796 | 				tasksPath,
 797 | 				taskId,
 798 | 				3,
 799 | 				false,
 800 | 				'',
 801 | 				context,
 802 | 				false
 803 | 			);
 804 | 
 805 | 			// Assert - Should successfully expand even 'done' tasks
 806 | 			expect(writeJSON).toHaveBeenCalled();
 807 | 			expect(result).toEqual(
 808 | 				expect.objectContaining({
 809 | 					task: expect.objectContaining({
 810 | 						id: 1,
 811 | 						status: 'done', // Status unchanged
 812 | 						subtasks: expect.arrayContaining([
 813 | 							expect.objectContaining({
 814 | 								id: 1,
 815 | 								title: 'Set up project structure',
 816 | 								status: 'pending'
 817 | 							})
 818 | 						])
 819 | 					}),
 820 | 					telemetryData: expect.any(Object)
 821 | 				})
 822 | 			);
 823 | 		});
 824 | 
 825 | 		test('should handle AI service failures', async () => {
 826 | 			// Arrange
 827 | 			const tasksPath = 'tasks/tasks.json';
 828 | 			const taskId = '2';
 829 | 			const context = {
 830 | 				mcpLog: createMcpLogMock(),
 831 | 				projectRoot: '/mock/project/root'
 832 | 			};
 833 | 
 834 | 			generateTextService.mockRejectedValueOnce(new Error('AI service error'));
 835 | 
 836 | 			// Act & Assert
 837 | 			await expect(
 838 | 				expandTask(tasksPath, taskId, 3, false, '', context, false)
 839 | 			).rejects.toThrow('AI service error');
 840 | 
 841 | 			expect(writeJSON).not.toHaveBeenCalled();
 842 | 		});
 843 | 
 844 | 		test('should handle file read errors', async () => {
 845 | 			// Arrange
 846 | 			const tasksPath = 'tasks/tasks.json';
 847 | 			const taskId = '2';
 848 | 			const context = {
 849 | 				mcpLog: createMcpLogMock(),
 850 | 				projectRoot: '/mock/project/root'
 851 | 			};
 852 | 
 853 | 			readJSON.mockImplementation(() => {
 854 | 				throw new Error('File read failed');
 855 | 			});
 856 | 
 857 | 			// Act & Assert
 858 | 			await expect(
 859 | 				expandTask(tasksPath, taskId, 3, false, '', context, false)
 860 | 			).rejects.toThrow('File read failed');
 861 | 
 862 | 			expect(writeJSON).not.toHaveBeenCalled();
 863 | 		});
 864 | 
 865 | 		test('should handle invalid tasks data', async () => {
 866 | 			// Arrange
 867 | 			const tasksPath = 'tasks/tasks.json';
 868 | 			const taskId = '2';
 869 | 			const context = {
 870 | 				mcpLog: createMcpLogMock(),
 871 | 				projectRoot: '/mock/project/root'
 872 | 			};
 873 | 
 874 | 			readJSON.mockReturnValue(null);
 875 | 
 876 | 			// Act & Assert
 877 | 			await expect(
 878 | 				expandTask(tasksPath, taskId, 3, false, '', context, false)
 879 | 			).rejects.toThrow();
 880 | 		});
 881 | 	});
 882 | 
 883 | 	describe('Output Format Handling', () => {
 884 | 		test('should display telemetry for CLI output format', async () => {
 885 | 			// Arrange
 886 | 			const { displayAiUsageSummary } = await import(
 887 | 				'../../../../../scripts/modules/ui.js'
 888 | 			);
 889 | 			const tasksPath = 'tasks/tasks.json';
 890 | 			const taskId = '2';
 891 | 			const context = {
 892 | 				projectRoot: '/mock/project/root'
 893 | 				// No mcpLog - should trigger CLI mode
 894 | 			};
 895 | 
 896 | 			// Act
 897 | 			await expandTask(tasksPath, taskId, 3, false, '', context, false);
 898 | 
 899 | 			// Assert - Should display telemetry for CLI users
 900 | 			expect(displayAiUsageSummary).toHaveBeenCalledWith(
 901 | 				expect.objectContaining({
 902 | 					commandName: 'expand-task',
 903 | 					modelUsed: 'claude-3-5-sonnet',
 904 | 					totalCost: 0.012414
 905 | 				}),
 906 | 				'cli'
 907 | 			);
 908 | 		});
 909 | 
 910 | 		test('should not display telemetry for MCP output format', async () => {
 911 | 			// Arrange
 912 | 			const { displayAiUsageSummary } = await import(
 913 | 				'../../../../../scripts/modules/ui.js'
 914 | 			);
 915 | 			const tasksPath = 'tasks/tasks.json';
 916 | 			const taskId = '2';
 917 | 			const context = {
 918 | 				mcpLog: createMcpLogMock(),
 919 | 				projectRoot: '/mock/project/root'
 920 | 			};
 921 | 
 922 | 			// Act
 923 | 			await expandTask(tasksPath, taskId, 3, false, '', context, false);
 924 | 
 925 | 			// Assert - Should NOT display telemetry for MCP (handled at higher level)
 926 | 			expect(displayAiUsageSummary).not.toHaveBeenCalled();
 927 | 		});
 928 | 	});
 929 | 
 930 | 	describe('Edge Cases', () => {
 931 | 		test('should handle empty additional context', async () => {
 932 | 			// Arrange
 933 | 			const tasksPath = 'tasks/tasks.json';
 934 | 			const taskId = '2';
 935 | 			const context = {
 936 | 				mcpLog: createMcpLogMock(),
 937 | 				projectRoot: '/mock/project/root'
 938 | 			};
 939 | 
 940 | 			// Act
 941 | 			await expandTask(tasksPath, taskId, 3, false, '', context, false);
 942 | 
 943 | 			// Assert - Should work with empty context (but may include project context)
 944 | 			expect(generateTextService).toHaveBeenCalledWith(
 945 | 				expect.objectContaining({
 946 | 					prompt: expect.stringMatching(/.*/) // Just ensure prompt exists
 947 | 				})
 948 | 			);
 949 | 		});
 950 | 
 951 | 		test('should handle additional context correctly', async () => {
 952 | 			// Arrange
 953 | 			const { getPromptManager } = await import(
 954 | 				'../../../../../scripts/modules/prompt-manager.js'
 955 | 			);
 956 | 			const mockLoadPrompt = jest.fn().mockResolvedValue({
 957 | 				systemPrompt: 'Mocked system prompt',
 958 | 				userPrompt: 'Mocked user prompt with context'
 959 | 			});
 960 | 			getPromptManager.mockReturnValue({
 961 | 				loadPrompt: mockLoadPrompt
 962 | 			});
 963 | 
 964 | 			const tasksPath = 'tasks/tasks.json';
 965 | 			const taskId = '2';
 966 | 			const additionalContext = 'Use React hooks and TypeScript';
 967 | 			const context = {
 968 | 				mcpLog: createMcpLogMock(),
 969 | 				projectRoot: '/mock/project/root'
 970 | 			};
 971 | 
 972 | 			// Act
 973 | 			await expandTask(
 974 | 				tasksPath,
 975 | 				taskId,
 976 | 				3,
 977 | 				false,
 978 | 				additionalContext,
 979 | 				context,
 980 | 				false
 981 | 			);
 982 | 
 983 | 			// Assert - Should pass separate context parameters to prompt manager
 984 | 			expect(mockLoadPrompt).toHaveBeenCalledWith(
 985 | 				'expand-task',
 986 | 				expect.objectContaining({
 987 | 					additionalContext: expect.stringContaining(
 988 | 						'Use React hooks and TypeScript'
 989 | 					),
 990 | 					gatheredContext: expect.stringContaining(
 991 | 						'Mock project context from files'
 992 | 					)
 993 | 				}),
 994 | 				expect.any(String)
 995 | 			);
 996 | 
 997 | 			// Additional assertion to verify the context parameters are passed separately
 998 | 			const call = mockLoadPrompt.mock.calls[0];
 999 | 			const parameters = call[1];
1000 | 			expect(parameters.additionalContext).toContain(
1001 | 				'Use React hooks and TypeScript'
1002 | 			);
1003 | 			expect(parameters.gatheredContext).toContain(
1004 | 				'Mock project context from files'
1005 | 			);
1006 | 		});
1007 | 
1008 | 		test('should handle missing project root in context', async () => {
1009 | 			// Arrange
1010 | 			const tasksPath = 'tasks/tasks.json';
1011 | 			const taskId = '2';
1012 | 			const context = {
1013 | 				mcpLog: createMcpLogMock()
1014 | 				// No projectRoot in context
1015 | 			};
1016 | 
1017 | 			// Act
1018 | 			await expandTask(tasksPath, taskId, 3, false, '', context, false);
1019 | 
1020 | 			// Assert - Should derive project root from tasksPath
1021 | 			expect(findProjectRoot).toHaveBeenCalledWith(tasksPath);
1022 | 			expect(readJSON).toHaveBeenCalledWith(
1023 | 				tasksPath,
1024 | 				'/mock/project/root',
1025 | 				undefined
1026 | 			);
1027 | 		});
1028 | 	});
1029 | 
1030 | 	describe('Dynamic Subtask Generation', () => {
1031 | 		const tasksPath = 'tasks/tasks.json';
1032 | 		const taskId = 1;
1033 | 		const context = { session: null, mcpLog: null };
1034 | 
1035 | 		beforeEach(() => {
1036 | 			// Reset all mocks
1037 | 			jest.clearAllMocks();
1038 | 
1039 | 			// Setup default mocks
1040 | 			readJSON.mockReturnValue({
1041 | 				tasks: [
1042 | 					{
1043 | 						id: 1,
1044 | 						title: 'Test Task',
1045 | 						description: 'A test task',
1046 | 						status: 'pending',
1047 | 						subtasks: []
1048 | 					}
1049 | 				]
1050 | 			});
1051 | 
1052 | 			findTaskById.mockReturnValue({
1053 | 				id: 1,
1054 | 				title: 'Test Task',
1055 | 				description: 'A test task',
1056 | 				status: 'pending',
1057 | 				subtasks: []
1058 | 			});
1059 | 
1060 | 			findProjectRoot.mockReturnValue('/mock/project/root');
1061 | 		});
1062 | 
1063 | 		test('should accept 0 as valid numSubtasks value for dynamic generation', async () => {
1064 | 			// Act - Call with numSubtasks=0 (should not throw error)
1065 | 			const result = await expandTask(
1066 | 				tasksPath,
1067 | 				taskId,
1068 | 				0,
1069 | 				false,
1070 | 				'',
1071 | 				context,
1072 | 				false
1073 | 			);
1074 | 
1075 | 			// Assert - Should complete successfully
1076 | 			expect(result).toBeDefined();
1077 | 			expect(generateTextService).toHaveBeenCalled();
1078 | 		});
1079 | 
1080 | 		test('should use dynamic prompting when numSubtasks is 0', async () => {
1081 | 			// Mock getPromptManager to return realistic prompt with dynamic content
1082 | 			const { getPromptManager } = await import(
1083 | 				'../../../../../scripts/modules/prompt-manager.js'
1084 | 			);
1085 | 			const mockLoadPrompt = jest.fn().mockResolvedValue({
1086 | 				systemPrompt:
1087 | 					'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into an appropriate number of specific subtasks that can be implemented one by one.',
1088 | 				userPrompt:
1089 | 					'Break down this task into an appropriate number of specific subtasks'
1090 | 			});
1091 | 			getPromptManager.mockReturnValue({
1092 | 				loadPrompt: mockLoadPrompt
1093 | 			});
1094 | 
1095 | 			// Act
1096 | 			await expandTask(tasksPath, taskId, 0, false, '', context, false);
1097 | 
1098 | 			// Assert - Verify generateTextService was called
1099 | 			expect(generateTextService).toHaveBeenCalled();
1100 | 
1101 | 			// Get the call arguments to verify the system prompt
1102 | 			const callArgs = generateTextService.mock.calls[0][0];
1103 | 			expect(callArgs.systemPrompt).toContain(
1104 | 				'an appropriate number of specific subtasks'
1105 | 			);
1106 | 		});
1107 | 
1108 | 		test('should use specific count prompting when numSubtasks is positive', async () => {
1109 | 			// Mock getPromptManager to return realistic prompt with specific count
1110 | 			const { getPromptManager } = await import(
1111 | 				'../../../../../scripts/modules/prompt-manager.js'
1112 | 			);
1113 | 			const mockLoadPrompt = jest.fn().mockResolvedValue({
1114 | 				systemPrompt:
1115 | 					'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 5 specific subtasks that can be implemented one by one.',
1116 | 				userPrompt: 'Break down this task into exactly 5 specific subtasks'
1117 | 			});
1118 | 			getPromptManager.mockReturnValue({
1119 | 				loadPrompt: mockLoadPrompt
1120 | 			});
1121 | 
1122 | 			// Act
1123 | 			await expandTask(tasksPath, taskId, 5, false, '', context, false);
1124 | 
1125 | 			// Assert - Verify generateTextService was called
1126 | 			expect(generateTextService).toHaveBeenCalled();
1127 | 
1128 | 			// Get the call arguments to verify the system prompt
1129 | 			const callArgs = generateTextService.mock.calls[0][0];
1130 | 			expect(callArgs.systemPrompt).toContain('5 specific subtasks');
1131 | 		});
1132 | 
1133 | 		test('should reject negative numSubtasks values and fallback to default', async () => {
1134 | 			// Mock getDefaultSubtasks to return a specific value
1135 | 			getDefaultSubtasks.mockReturnValue(4);
1136 | 
1137 | 			// Mock getPromptManager to return realistic prompt with default count
1138 | 			const { getPromptManager } = await import(
1139 | 				'../../../../../scripts/modules/prompt-manager.js'
1140 | 			);
1141 | 			const mockLoadPrompt = jest.fn().mockResolvedValue({
1142 | 				systemPrompt:
1143 | 					'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 4 specific subtasks that can be implemented one by one.',
1144 | 				userPrompt: 'Break down this task into exactly 4 specific subtasks'
1145 | 			});
1146 | 			getPromptManager.mockReturnValue({
1147 | 				loadPrompt: mockLoadPrompt
1148 | 			});
1149 | 
1150 | 			// Act
1151 | 			await expandTask(tasksPath, taskId, -3, false, '', context, false);
1152 | 
1153 | 			// Assert - Should use default value instead of negative
1154 | 			expect(generateTextService).toHaveBeenCalled();
1155 | 			const callArgs = generateTextService.mock.calls[0][0];
1156 | 			expect(callArgs.systemPrompt).toContain('4 specific subtasks');
1157 | 		});
1158 | 
1159 | 		test('should use getDefaultSubtasks when numSubtasks is undefined', async () => {
1160 | 			// Mock getDefaultSubtasks to return a specific value
1161 | 			getDefaultSubtasks.mockReturnValue(6);
1162 | 
1163 | 			// Mock getPromptManager to return realistic prompt with default count
1164 | 			const { getPromptManager } = await import(
1165 | 				'../../../../../scripts/modules/prompt-manager.js'
1166 | 			);
1167 | 			const mockLoadPrompt = jest.fn().mockResolvedValue({
1168 | 				systemPrompt:
1169 | 					'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 6 specific subtasks that can be implemented one by one.',
1170 | 				userPrompt: 'Break down this task into exactly 6 specific subtasks'
1171 | 			});
1172 | 			getPromptManager.mockReturnValue({
1173 | 				loadPrompt: mockLoadPrompt
1174 | 			});
1175 | 
1176 | 			// Act - Call without specifying numSubtasks (undefined)
1177 | 			await expandTask(tasksPath, taskId, undefined, false, '', context, false);
1178 | 
1179 | 			// Assert - Should use default value
1180 | 			expect(generateTextService).toHaveBeenCalled();
1181 | 			const callArgs = generateTextService.mock.calls[0][0];
1182 | 			expect(callArgs.systemPrompt).toContain('6 specific subtasks');
1183 | 		});
1184 | 
1185 | 		test('should use getDefaultSubtasks when numSubtasks is null', async () => {
1186 | 			// Mock getDefaultSubtasks to return a specific value
1187 | 			getDefaultSubtasks.mockReturnValue(7);
1188 | 
1189 | 			// Mock getPromptManager to return realistic prompt with default count
1190 | 			const { getPromptManager } = await import(
1191 | 				'../../../../../scripts/modules/prompt-manager.js'
1192 | 			);
1193 | 			const mockLoadPrompt = jest.fn().mockResolvedValue({
1194 | 				systemPrompt:
1195 | 					'You are an AI assistant helping with task breakdown for software development. You need to break down a high-level task into 7 specific subtasks that can be implemented one by one.',
1196 | 				userPrompt: 'Break down this task into exactly 7 specific subtasks'
1197 | 			});
1198 | 			getPromptManager.mockReturnValue({
1199 | 				loadPrompt: mockLoadPrompt
1200 | 			});
1201 | 
1202 | 			// Act - Call with null numSubtasks
1203 | 			await expandTask(tasksPath, taskId, null, false, '', context, false);
1204 | 
1205 | 			// Assert - Should use default value
1206 | 			expect(generateTextService).toHaveBeenCalled();
1207 | 			const callArgs = generateTextService.mock.calls[0][0];
1208 | 			expect(callArgs.systemPrompt).toContain('7 specific subtasks');
1209 | 		});
1210 | 	});
1211 | });
1212 | 
```

--------------------------------------------------------------------------------
/scripts/modules/config-manager.js:
--------------------------------------------------------------------------------

```javascript
   1 | import fs from 'fs';
   2 | import path from 'path';
   3 | import { fileURLToPath } from 'url';
   4 | import chalk from 'chalk';
   5 | import { z } from 'zod';
   6 | import { AI_COMMAND_NAMES } from '../../src/constants/commands.js';
   7 | import {
   8 | 	LEGACY_CONFIG_FILE,
   9 | 	TASKMASTER_DIR
  10 | } from '../../src/constants/paths.js';
  11 | import {
  12 | 	ALL_PROVIDERS,
  13 | 	CUSTOM_PROVIDERS,
  14 | 	CUSTOM_PROVIDERS_ARRAY,
  15 | 	VALIDATED_PROVIDERS
  16 | } from '../../src/constants/providers.js';
  17 | import { findConfigPath } from '../../src/utils/path-utils.js';
  18 | import { findProjectRoot, isEmpty, log, resolveEnvVariable } from './utils.js';
  19 | import MODEL_MAP from './supported-models.json' with { type: 'json' };
  20 | 
  21 | // Calculate __dirname in ESM
  22 | const __filename = fileURLToPath(import.meta.url);
  23 | const __dirname = path.dirname(__filename);
  24 | 
  25 | // Default configuration values (used if config file is missing or incomplete)
  26 | const DEFAULTS = {
  27 | 	models: {
  28 | 		main: {
  29 | 			provider: 'anthropic',
  30 | 			modelId: 'claude-sonnet-4-20250514',
  31 | 			maxTokens: 64000,
  32 | 			temperature: 0.2
  33 | 		},
  34 | 		research: {
  35 | 			provider: 'perplexity',
  36 | 			modelId: 'sonar',
  37 | 			maxTokens: 8700,
  38 | 			temperature: 0.1
  39 | 		},
  40 | 		fallback: {
  41 | 			// No default fallback provider/model initially
  42 | 			provider: 'anthropic',
  43 | 			modelId: 'claude-3-7-sonnet-20250219',
  44 | 			maxTokens: 120000, // Default parameters if fallback IS configured
  45 | 			temperature: 0.2
  46 | 		}
  47 | 	},
  48 | 	global: {
  49 | 		logLevel: 'info',
  50 | 		debug: false,
  51 | 		defaultNumTasks: 10,
  52 | 		defaultSubtasks: 5,
  53 | 		defaultPriority: 'medium',
  54 | 		projectName: 'Task Master',
  55 | 		ollamaBaseURL: 'http://localhost:11434/api',
  56 | 		bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
  57 | 		responseLanguage: 'English',
  58 | 		enableCodebaseAnalysis: true
  59 | 	},
  60 | 	claudeCode: {},
  61 | 	grokCli: {
  62 | 		timeout: 120000,
  63 | 		workingDirectory: null,
  64 | 		defaultModel: 'grok-4-latest'
  65 | 	}
  66 | };
  67 | 
  68 | // --- Internal Config Loading ---
  69 | let loadedConfig = null;
  70 | let loadedConfigRoot = null; // Track which root loaded the config
  71 | 
  72 | // Custom Error for configuration issues
  73 | class ConfigurationError extends Error {
  74 | 	constructor(message) {
  75 | 		super(message);
  76 | 		this.name = 'ConfigurationError';
  77 | 	}
  78 | }
  79 | 
  80 | function _loadAndValidateConfig(explicitRoot = null) {
  81 | 	const defaults = DEFAULTS; // Use the defined defaults
  82 | 	let rootToUse = explicitRoot;
  83 | 	let configSource = explicitRoot
  84 | 		? `explicit root (${explicitRoot})`
  85 | 		: 'defaults (no root provided yet)';
  86 | 
  87 | 	// ---> If no explicit root, TRY to find it <---
  88 | 	if (!rootToUse) {
  89 | 		rootToUse = findProjectRoot();
  90 | 		if (rootToUse) {
  91 | 			configSource = `found root (${rootToUse})`;
  92 | 		} else {
  93 | 			// No root found, use current working directory as fallback
  94 | 			// This prevents infinite loops during initialization
  95 | 			rootToUse = process.cwd();
  96 | 			configSource = `current directory (${rootToUse}) - no project markers found`;
  97 | 		}
  98 | 	}
  99 | 	// ---> End find project root logic <---
 100 | 
 101 | 	// --- Find configuration file ---
 102 | 	let configPath = null;
 103 | 	let config = { ...defaults }; // Start with a deep copy of defaults
 104 | 	let configExists = false;
 105 | 
 106 | 	// During initialization (no project markers), skip config file search entirely
 107 | 	const hasProjectMarkers =
 108 | 		fs.existsSync(path.join(rootToUse, TASKMASTER_DIR)) ||
 109 | 		fs.existsSync(path.join(rootToUse, LEGACY_CONFIG_FILE));
 110 | 
 111 | 	if (hasProjectMarkers) {
 112 | 		// Only try to find config if we have project markers
 113 | 		// This prevents the repeated warnings during init
 114 | 		configPath = findConfigPath(null, { projectRoot: rootToUse });
 115 | 	}
 116 | 
 117 | 	if (configPath) {
 118 | 		configExists = true;
 119 | 		const isLegacy = configPath.endsWith(LEGACY_CONFIG_FILE);
 120 | 
 121 | 		try {
 122 | 			const rawData = fs.readFileSync(configPath, 'utf-8');
 123 | 			const parsedConfig = JSON.parse(rawData);
 124 | 
 125 | 			// Deep merge parsed config onto defaults
 126 | 			config = {
 127 | 				models: {
 128 | 					main: { ...defaults.models.main, ...parsedConfig?.models?.main },
 129 | 					research: {
 130 | 						...defaults.models.research,
 131 | 						...parsedConfig?.models?.research
 132 | 					},
 133 | 					fallback:
 134 | 						parsedConfig?.models?.fallback?.provider &&
 135 | 						parsedConfig?.models?.fallback?.modelId
 136 | 							? { ...defaults.models.fallback, ...parsedConfig.models.fallback }
 137 | 							: { ...defaults.models.fallback }
 138 | 				},
 139 | 				global: { ...defaults.global, ...parsedConfig?.global },
 140 | 				claudeCode: { ...defaults.claudeCode, ...parsedConfig?.claudeCode },
 141 | 				grokCli: { ...defaults.grokCli, ...parsedConfig?.grokCli }
 142 | 			};
 143 | 			configSource = `file (${configPath})`; // Update source info
 144 | 
 145 | 			// Issue deprecation warning if using legacy config file
 146 | 			if (isLegacy) {
 147 | 				console.warn(
 148 | 					chalk.yellow(
 149 | 						`⚠️  DEPRECATION WARNING: Found configuration in legacy location '${configPath}'. Please migrate to .taskmaster/config.json. Run 'task-master migrate' to automatically migrate your project.`
 150 | 					)
 151 | 				);
 152 | 			}
 153 | 
 154 | 			// --- Validation (Warn if file content is invalid) ---
 155 | 			// Use log.warn for consistency
 156 | 			if (!validateProvider(config.models.main.provider)) {
 157 | 				console.warn(
 158 | 					chalk.yellow(
 159 | 						`Warning: Invalid main provider "${config.models.main.provider}" in ${configPath}. Falling back to default.`
 160 | 					)
 161 | 				);
 162 | 				config.models.main = { ...defaults.models.main };
 163 | 			}
 164 | 			if (!validateProvider(config.models.research.provider)) {
 165 | 				console.warn(
 166 | 					chalk.yellow(
 167 | 						`Warning: Invalid research provider "${config.models.research.provider}" in ${configPath}. Falling back to default.`
 168 | 					)
 169 | 				);
 170 | 				config.models.research = { ...defaults.models.research };
 171 | 			}
 172 | 			if (
 173 | 				config.models.fallback?.provider &&
 174 | 				!validateProvider(config.models.fallback.provider)
 175 | 			) {
 176 | 				console.warn(
 177 | 					chalk.yellow(
 178 | 						`Warning: Invalid fallback provider "${config.models.fallback.provider}" in ${configPath}. Fallback model configuration will be ignored.`
 179 | 					)
 180 | 				);
 181 | 				config.models.fallback.provider = undefined;
 182 | 				config.models.fallback.modelId = undefined;
 183 | 			}
 184 | 			if (config.claudeCode && !isEmpty(config.claudeCode)) {
 185 | 				config.claudeCode = validateClaudeCodeSettings(config.claudeCode);
 186 | 			}
 187 | 		} catch (error) {
 188 | 			// Use console.error for actual errors during parsing
 189 | 			console.error(
 190 | 				chalk.red(
 191 | 					`Error reading or parsing ${configPath}: ${error.message}. Using default configuration.`
 192 | 				)
 193 | 			);
 194 | 			config = { ...defaults }; // Reset to defaults on parse error
 195 | 			configSource = `defaults (parse error at ${configPath})`;
 196 | 		}
 197 | 	} else {
 198 | 		// Config file doesn't exist at the determined rootToUse.
 199 | 		if (explicitRoot) {
 200 | 			// Only warn if an explicit root was *expected*.
 201 | 			console.warn(
 202 | 				chalk.yellow(
 203 | 					`Warning: Configuration file not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.`
 204 | 				)
 205 | 			);
 206 | 		} else {
 207 | 			// Don't warn about missing config during initialization
 208 | 			// Only warn if this looks like an existing project (has .taskmaster dir or legacy config marker)
 209 | 			const hasTaskmasterDir = fs.existsSync(
 210 | 				path.join(rootToUse, TASKMASTER_DIR)
 211 | 			);
 212 | 			const hasLegacyMarker = fs.existsSync(
 213 | 				path.join(rootToUse, LEGACY_CONFIG_FILE)
 214 | 			);
 215 | 
 216 | 			if (hasTaskmasterDir || hasLegacyMarker) {
 217 | 				console.warn(
 218 | 					chalk.yellow(
 219 | 						`Warning: Configuration file not found at derived root (${rootToUse}). Using defaults.`
 220 | 					)
 221 | 				);
 222 | 			}
 223 | 		}
 224 | 		// Keep config as defaults
 225 | 		config = { ...defaults };
 226 | 		configSource = `defaults (no config file found at ${rootToUse})`;
 227 | 	}
 228 | 
 229 | 	return config;
 230 | }
 231 | 
 232 | /**
 233 |  * Gets the current configuration, loading it if necessary.
 234 |  * Handles MCP initialization context gracefully.
 235 |  * @param {string|null} explicitRoot - Optional explicit path to the project root.
 236 |  * @param {boolean} forceReload - Force reloading the config file.
 237 |  * @returns {object} The loaded configuration object.
 238 |  */
 239 | function getConfig(explicitRoot = null, forceReload = false) {
 240 | 	// Determine if a reload is necessary
 241 | 	const needsLoad =
 242 | 		!loadedConfig ||
 243 | 		forceReload ||
 244 | 		(explicitRoot && explicitRoot !== loadedConfigRoot);
 245 | 
 246 | 	if (needsLoad) {
 247 | 		const newConfig = _loadAndValidateConfig(explicitRoot); // _load handles null explicitRoot
 248 | 
 249 | 		// Only update the global cache if loading was forced or if an explicit root
 250 | 		// was provided (meaning we attempted to load a specific project's config).
 251 | 		// We avoid caching the initial default load triggered without an explicitRoot.
 252 | 		if (forceReload || explicitRoot) {
 253 | 			loadedConfig = newConfig;
 254 | 			loadedConfigRoot = explicitRoot; // Store the root used for this loaded config
 255 | 		}
 256 | 		return newConfig; // Return the newly loaded/default config
 257 | 	}
 258 | 
 259 | 	// If no load was needed, return the cached config
 260 | 	return loadedConfig;
 261 | }
 262 | 
 263 | /**
 264 |  * Validates if a provider name is supported.
 265 |  * Custom providers (azure, vertex, bedrock, openrouter, ollama) are always allowed.
 266 |  * Validated providers must exist in the MODEL_MAP from supported-models.json.
 267 |  * @param {string} providerName The name of the provider.
 268 |  * @returns {boolean} True if the provider is valid, false otherwise.
 269 |  */
 270 | function validateProvider(providerName) {
 271 | 	// Custom providers are always allowed
 272 | 	if (CUSTOM_PROVIDERS_ARRAY.includes(providerName)) {
 273 | 		return true;
 274 | 	}
 275 | 
 276 | 	// Validated providers must exist in MODEL_MAP
 277 | 	if (VALIDATED_PROVIDERS.includes(providerName)) {
 278 | 		return !!(MODEL_MAP && MODEL_MAP[providerName]);
 279 | 	}
 280 | 
 281 | 	// Unknown providers are not allowed
 282 | 	return false;
 283 | }
 284 | 
 285 | /**
 286 |  * Optional: Validates if a modelId is known for a given provider based on MODEL_MAP.
 287 |  * This is a non-strict validation; an unknown model might still be valid.
 288 |  * @param {string} providerName The name of the provider.
 289 |  * @param {string} modelId The model ID.
 290 |  * @returns {boolean} True if the modelId is in the map for the provider, false otherwise.
 291 |  */
 292 | function validateProviderModelCombination(providerName, modelId) {
 293 | 	// If provider isn't even in our map, we can't validate the model
 294 | 	if (!MODEL_MAP[providerName]) {
 295 | 		return true; // Allow unknown providers or those without specific model lists
 296 | 	}
 297 | 	// If the provider is known, check if the model is in its list OR if the list is empty (meaning accept any)
 298 | 	return (
 299 | 		MODEL_MAP[providerName].length === 0 ||
 300 | 		// Use .some() to check the 'id' property of objects in the array
 301 | 		MODEL_MAP[providerName].some((modelObj) => modelObj.id === modelId)
 302 | 	);
 303 | }
 304 | 
 305 | /**
 306 |  * Validates Claude Code AI provider custom settings
 307 |  * @param {object} settings The settings to validate
 308 |  * @returns {object} The validated settings
 309 |  */
 310 | function validateClaudeCodeSettings(settings) {
 311 | 	// Define the base settings schema without commandSpecific first
 312 | 	const BaseSettingsSchema = z.object({
 313 | 		maxTurns: z.number().int().positive().optional(),
 314 | 		customSystemPrompt: z.string().optional(),
 315 | 		appendSystemPrompt: z.string().optional(),
 316 | 		permissionMode: z
 317 | 			.enum(['default', 'acceptEdits', 'plan', 'bypassPermissions'])
 318 | 			.optional(),
 319 | 		allowedTools: z.array(z.string()).optional(),
 320 | 		disallowedTools: z.array(z.string()).optional(),
 321 | 		mcpServers: z
 322 | 			.record(
 323 | 				z.string(),
 324 | 				z.object({
 325 | 					type: z.enum(['stdio', 'sse']).optional(),
 326 | 					command: z.string(),
 327 | 					args: z.array(z.string()).optional(),
 328 | 					env: z.record(z.string()).optional(),
 329 | 					url: z.string().url().optional(),
 330 | 					headers: z.record(z.string()).optional()
 331 | 				})
 332 | 			)
 333 | 			.optional()
 334 | 	});
 335 | 
 336 | 	// Define CommandSpecificSchema using the base schema
 337 | 	const CommandSpecificSchema = z.record(
 338 | 		z.enum(AI_COMMAND_NAMES),
 339 | 		BaseSettingsSchema
 340 | 	);
 341 | 
 342 | 	// Define the full settings schema with commandSpecific
 343 | 	const SettingsSchema = BaseSettingsSchema.extend({
 344 | 		commandSpecific: CommandSpecificSchema.optional()
 345 | 	});
 346 | 
 347 | 	let validatedSettings = {};
 348 | 
 349 | 	try {
 350 | 		validatedSettings = SettingsSchema.parse(settings);
 351 | 	} catch (error) {
 352 | 		console.warn(
 353 | 			chalk.yellow(
 354 | 				`Warning: Invalid Claude Code settings in config: ${error.message}. Falling back to default.`
 355 | 			)
 356 | 		);
 357 | 
 358 | 		validatedSettings = {};
 359 | 	}
 360 | 
 361 | 	return validatedSettings;
 362 | }
 363 | 
 364 | // --- Claude Code Settings Getters ---
 365 | 
 366 | function getClaudeCodeSettings(explicitRoot = null, forceReload = false) {
 367 | 	const config = getConfig(explicitRoot, forceReload);
 368 | 	// Ensure Claude Code defaults are applied if Claude Code section is missing
 369 | 	return { ...DEFAULTS.claudeCode, ...(config?.claudeCode || {}) };
 370 | }
 371 | 
 372 | function getClaudeCodeSettingsForCommand(
 373 | 	commandName,
 374 | 	explicitRoot = null,
 375 | 	forceReload = false
 376 | ) {
 377 | 	const settings = getClaudeCodeSettings(explicitRoot, forceReload);
 378 | 	const commandSpecific = settings?.commandSpecific || {};
 379 | 	return { ...settings, ...commandSpecific[commandName] };
 380 | }
 381 | 
 382 | function getGrokCliSettings(explicitRoot = null, forceReload = false) {
 383 | 	const config = getConfig(explicitRoot, forceReload);
 384 | 	// Ensure Grok CLI defaults are applied if Grok CLI section is missing
 385 | 	return { ...DEFAULTS.grokCli, ...(config?.grokCli || {}) };
 386 | }
 387 | 
 388 | function getGrokCliSettingsForCommand(
 389 | 	commandName,
 390 | 	explicitRoot = null,
 391 | 	forceReload = false
 392 | ) {
 393 | 	const settings = getGrokCliSettings(explicitRoot, forceReload);
 394 | 	const commandSpecific = settings?.commandSpecific || {};
 395 | 	return { ...settings, ...commandSpecific[commandName] };
 396 | }
 397 | 
 398 | // --- Role-Specific Getters ---
 399 | 
 400 | function getModelConfigForRole(role, explicitRoot = null) {
 401 | 	const config = getConfig(explicitRoot);
 402 | 	const roleConfig = config?.models?.[role];
 403 | 	if (!roleConfig) {
 404 | 		log(
 405 | 			'warn',
 406 | 			`No model configuration found for role: ${role}. Returning default.`
 407 | 		);
 408 | 		return DEFAULTS.models[role] || {};
 409 | 	}
 410 | 	return roleConfig;
 411 | }
 412 | 
 413 | function getMainProvider(explicitRoot = null) {
 414 | 	return getModelConfigForRole('main', explicitRoot).provider;
 415 | }
 416 | 
 417 | function getMainModelId(explicitRoot = null) {
 418 | 	return getModelConfigForRole('main', explicitRoot).modelId;
 419 | }
 420 | 
 421 | function getMainMaxTokens(explicitRoot = null) {
 422 | 	// Directly return value from config (which includes defaults)
 423 | 	return getModelConfigForRole('main', explicitRoot).maxTokens;
 424 | }
 425 | 
 426 | function getMainTemperature(explicitRoot = null) {
 427 | 	// Directly return value from config
 428 | 	return getModelConfigForRole('main', explicitRoot).temperature;
 429 | }
 430 | 
 431 | function getResearchProvider(explicitRoot = null) {
 432 | 	return getModelConfigForRole('research', explicitRoot).provider;
 433 | }
 434 | 
 435 | /**
 436 |  * Check if codebase analysis feature flag is enabled across all sources
 437 |  * Priority: .env > MCP env > config.json
 438 |  * @param {object|null} session - MCP session object (optional)
 439 |  * @param {string|null} projectRoot - Project root path (optional)
 440 |  * @returns {boolean} True if codebase analysis is enabled
 441 |  */
 442 | function isCodebaseAnalysisEnabled(session = null, projectRoot = null) {
 443 | 	// Priority 1: Environment variable
 444 | 	const envFlag = resolveEnvVariable(
 445 | 		'TASKMASTER_ENABLE_CODEBASE_ANALYSIS',
 446 | 		session,
 447 | 		projectRoot
 448 | 	);
 449 | 	if (envFlag !== null && envFlag !== undefined && envFlag !== '') {
 450 | 		return envFlag.toLowerCase() === 'true' || envFlag === '1';
 451 | 	}
 452 | 
 453 | 	// Priority 2: MCP session environment
 454 | 	if (session?.env?.TASKMASTER_ENABLE_CODEBASE_ANALYSIS) {
 455 | 		const mcpFlag = session.env.TASKMASTER_ENABLE_CODEBASE_ANALYSIS;
 456 | 		return mcpFlag.toLowerCase() === 'true' || mcpFlag === '1';
 457 | 	}
 458 | 
 459 | 	// Priority 3: Configuration file
 460 | 	const globalConfig = getGlobalConfig(projectRoot);
 461 | 	return globalConfig.enableCodebaseAnalysis !== false; // Default to true
 462 | }
 463 | 
 464 | /**
 465 |  * Check if codebase analysis is available and enabled
 466 |  * @param {boolean} useResearch - Whether to check research provider or main provider
 467 |  * @param {string|null} projectRoot - Project root path (optional)
 468 |  * @param {object|null} session - MCP session object (optional)
 469 |  * @returns {boolean} True if codebase analysis is available and enabled
 470 |  */
 471 | function hasCodebaseAnalysis(
 472 | 	useResearch = false,
 473 | 	projectRoot = null,
 474 | 	session = null
 475 | ) {
 476 | 	// First check if the feature is enabled
 477 | 	if (!isCodebaseAnalysisEnabled(session, projectRoot)) {
 478 | 		return false;
 479 | 	}
 480 | 
 481 | 	// Then check if a codebase analysis provider is configured
 482 | 	const currentProvider = useResearch
 483 | 		? getResearchProvider(projectRoot)
 484 | 		: getMainProvider(projectRoot);
 485 | 
 486 | 	return (
 487 | 		currentProvider === CUSTOM_PROVIDERS.CLAUDE_CODE ||
 488 | 		currentProvider === CUSTOM_PROVIDERS.GEMINI_CLI ||
 489 | 		currentProvider === CUSTOM_PROVIDERS.GROK_CLI
 490 | 	);
 491 | }
 492 | 
 493 | function getResearchModelId(explicitRoot = null) {
 494 | 	return getModelConfigForRole('research', explicitRoot).modelId;
 495 | }
 496 | 
 497 | function getResearchMaxTokens(explicitRoot = null) {
 498 | 	// Directly return value from config
 499 | 	return getModelConfigForRole('research', explicitRoot).maxTokens;
 500 | }
 501 | 
 502 | function getResearchTemperature(explicitRoot = null) {
 503 | 	// Directly return value from config
 504 | 	return getModelConfigForRole('research', explicitRoot).temperature;
 505 | }
 506 | 
 507 | function getFallbackProvider(explicitRoot = null) {
 508 | 	// Directly return value from config (will be undefined if not set)
 509 | 	return getModelConfigForRole('fallback', explicitRoot).provider;
 510 | }
 511 | 
 512 | function getFallbackModelId(explicitRoot = null) {
 513 | 	// Directly return value from config
 514 | 	return getModelConfigForRole('fallback', explicitRoot).modelId;
 515 | }
 516 | 
 517 | function getFallbackMaxTokens(explicitRoot = null) {
 518 | 	// Directly return value from config
 519 | 	return getModelConfigForRole('fallback', explicitRoot).maxTokens;
 520 | }
 521 | 
 522 | function getFallbackTemperature(explicitRoot = null) {
 523 | 	// Directly return value from config
 524 | 	return getModelConfigForRole('fallback', explicitRoot).temperature;
 525 | }
 526 | 
 527 | // --- Global Settings Getters ---
 528 | 
 529 | function getGlobalConfig(explicitRoot = null) {
 530 | 	const config = getConfig(explicitRoot);
 531 | 	// Ensure global defaults are applied if global section is missing
 532 | 	return { ...DEFAULTS.global, ...(config?.global || {}) };
 533 | }
 534 | 
 535 | function getLogLevel(explicitRoot = null) {
 536 | 	// Directly return value from config
 537 | 	return getGlobalConfig(explicitRoot).logLevel.toLowerCase();
 538 | }
 539 | 
 540 | function getDebugFlag(explicitRoot = null) {
 541 | 	// Directly return value from config, ensure boolean
 542 | 	return getGlobalConfig(explicitRoot).debug === true;
 543 | }
 544 | 
 545 | function getDefaultSubtasks(explicitRoot = null) {
 546 | 	// Directly return value from config, ensure integer
 547 | 	const val = getGlobalConfig(explicitRoot).defaultSubtasks;
 548 | 	const parsedVal = parseInt(val, 10);
 549 | 	return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultSubtasks : parsedVal;
 550 | }
 551 | 
 552 | function getDefaultNumTasks(explicitRoot = null) {
 553 | 	const val = getGlobalConfig(explicitRoot).defaultNumTasks;
 554 | 	const parsedVal = parseInt(val, 10);
 555 | 	return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultNumTasks : parsedVal;
 556 | }
 557 | 
 558 | function getDefaultPriority(explicitRoot = null) {
 559 | 	// Directly return value from config
 560 | 	return getGlobalConfig(explicitRoot).defaultPriority;
 561 | }
 562 | 
 563 | function getProjectName(explicitRoot = null) {
 564 | 	// Directly return value from config
 565 | 	return getGlobalConfig(explicitRoot).projectName;
 566 | }
 567 | 
 568 | function getOllamaBaseURL(explicitRoot = null) {
 569 | 	// Directly return value from config
 570 | 	return getGlobalConfig(explicitRoot).ollamaBaseURL;
 571 | }
 572 | 
 573 | function getAzureBaseURL(explicitRoot = null) {
 574 | 	// Directly return value from config
 575 | 	return getGlobalConfig(explicitRoot).azureBaseURL;
 576 | }
 577 | 
 578 | function getBedrockBaseURL(explicitRoot = null) {
 579 | 	// Directly return value from config
 580 | 	return getGlobalConfig(explicitRoot).bedrockBaseURL;
 581 | }
 582 | 
 583 | /**
 584 |  * Gets the Google Cloud project ID for Vertex AI from configuration
 585 |  * @param {string|null} explicitRoot - Optional explicit path to the project root.
 586 |  * @returns {string|null} The project ID or null if not configured
 587 |  */
 588 | function getVertexProjectId(explicitRoot = null) {
 589 | 	// Return value from config
 590 | 	return getGlobalConfig(explicitRoot).vertexProjectId;
 591 | }
 592 | 
 593 | /**
 594 |  * Gets the Google Cloud location for Vertex AI from configuration
 595 |  * @param {string|null} explicitRoot - Optional explicit path to the project root.
 596 |  * @returns {string} The location or default value of "us-central1"
 597 |  */
 598 | function getVertexLocation(explicitRoot = null) {
 599 | 	// Return value from config or default
 600 | 	return getGlobalConfig(explicitRoot).vertexLocation || 'us-central1';
 601 | }
 602 | 
 603 | function getResponseLanguage(explicitRoot = null) {
 604 | 	// Directly return value from config
 605 | 	return getGlobalConfig(explicitRoot).responseLanguage;
 606 | }
 607 | 
 608 | function getCodebaseAnalysisEnabled(explicitRoot = null) {
 609 | 	// Return boolean-safe value with default true
 610 | 	return getGlobalConfig(explicitRoot).enableCodebaseAnalysis !== false;
 611 | }
 612 | 
 613 | /**
 614 |  * Gets model parameters (maxTokens, temperature) for a specific role,
 615 |  * considering model-specific overrides from supported-models.json.
 616 |  * @param {string} role - The role ('main', 'research', 'fallback').
 617 |  * @param {string|null} explicitRoot - Optional explicit path to the project root.
 618 |  * @returns {{maxTokens: number, temperature: number}}
 619 |  */
 620 | function getParametersForRole(role, explicitRoot = null) {
 621 | 	const roleConfig = getModelConfigForRole(role, explicitRoot);
 622 | 	const roleMaxTokens = roleConfig.maxTokens;
 623 | 	const roleTemperature = roleConfig.temperature;
 624 | 	const modelId = roleConfig.modelId;
 625 | 	const providerName = roleConfig.provider;
 626 | 
 627 | 	let effectiveMaxTokens = roleMaxTokens; // Start with the role's default
 628 | 	let effectiveTemperature = roleTemperature; // Start with the role's default
 629 | 
 630 | 	try {
 631 | 		// Find the model definition in MODEL_MAP
 632 | 		const providerModels = MODEL_MAP[providerName];
 633 | 		if (providerModels && Array.isArray(providerModels)) {
 634 | 			const modelDefinition = providerModels.find((m) => m.id === modelId);
 635 | 
 636 | 			// Check if a model-specific max_tokens is defined and valid
 637 | 			if (
 638 | 				modelDefinition &&
 639 | 				typeof modelDefinition.max_tokens === 'number' &&
 640 | 				modelDefinition.max_tokens > 0
 641 | 			) {
 642 | 				const modelSpecificMaxTokens = modelDefinition.max_tokens;
 643 | 				// Use the minimum of the role default and the model specific limit
 644 | 				effectiveMaxTokens = Math.min(roleMaxTokens, modelSpecificMaxTokens);
 645 | 				log(
 646 | 					'debug',
 647 | 					`Applying model-specific max_tokens (${modelSpecificMaxTokens}) for ${modelId}. Effective limit: ${effectiveMaxTokens}`
 648 | 				);
 649 | 			} else {
 650 | 				log(
 651 | 					'debug',
 652 | 					`No valid model-specific max_tokens override found for ${modelId}. Using role default: ${roleMaxTokens}`
 653 | 				);
 654 | 			}
 655 | 
 656 | 			// Check if a model-specific temperature is defined
 657 | 			if (
 658 | 				modelDefinition &&
 659 | 				typeof modelDefinition.temperature === 'number' &&
 660 | 				modelDefinition.temperature >= 0 &&
 661 | 				modelDefinition.temperature <= 1
 662 | 			) {
 663 | 				effectiveTemperature = modelDefinition.temperature;
 664 | 				log(
 665 | 					'debug',
 666 | 					`Applying model-specific temperature (${modelDefinition.temperature}) for ${modelId}`
 667 | 				);
 668 | 			}
 669 | 		} else {
 670 | 			// Special handling for custom OpenRouter models
 671 | 			if (providerName === CUSTOM_PROVIDERS.OPENROUTER) {
 672 | 				// Use a conservative default for OpenRouter models not in our list
 673 | 				const openrouterDefault = 32768;
 674 | 				effectiveMaxTokens = Math.min(roleMaxTokens, openrouterDefault);
 675 | 				log(
 676 | 					'debug',
 677 | 					`Custom OpenRouter model ${modelId} detected. Using conservative max_tokens: ${effectiveMaxTokens}`
 678 | 				);
 679 | 			} else {
 680 | 				log(
 681 | 					'debug',
 682 | 					`No model definitions found for provider ${providerName} in MODEL_MAP. Using role default maxTokens: ${roleMaxTokens}`
 683 | 				);
 684 | 			}
 685 | 		}
 686 | 	} catch (lookupError) {
 687 | 		log(
 688 | 			'warn',
 689 | 			`Error looking up model-specific parameters for ${modelId}: ${lookupError.message}. Using role defaults.`
 690 | 		);
 691 | 		// Fallback to role defaults on error
 692 | 		effectiveMaxTokens = roleMaxTokens;
 693 | 		effectiveTemperature = roleTemperature;
 694 | 	}
 695 | 
 696 | 	return {
 697 | 		maxTokens: effectiveMaxTokens,
 698 | 		temperature: effectiveTemperature
 699 | 	};
 700 | }
 701 | 
 702 | /**
 703 |  * Checks if the API key for a given provider is set in the environment.
 704 |  * Checks process.env first, then session.env if session is provided, then .env file if projectRoot provided.
 705 |  * @param {string} providerName - The name of the provider (e.g., 'openai', 'anthropic').
 706 |  * @param {object|null} [session=null] - The MCP session object (optional).
 707 |  * @param {string|null} [projectRoot=null] - The project root directory (optional, for .env file check).
 708 |  * @returns {boolean} True if the API key is set, false otherwise.
 709 |  */
 710 | function isApiKeySet(providerName, session = null, projectRoot = null) {
 711 | 	// Define the expected environment variable name for each provider
 712 | 
 713 | 	// Providers that don't require API keys for authentication
 714 | 	const providersWithoutApiKeys = [
 715 | 		CUSTOM_PROVIDERS.OLLAMA,
 716 | 		CUSTOM_PROVIDERS.BEDROCK,
 717 | 		CUSTOM_PROVIDERS.MCP,
 718 | 		CUSTOM_PROVIDERS.GEMINI_CLI,
 719 | 		CUSTOM_PROVIDERS.GROK_CLI
 720 | 	];
 721 | 
 722 | 	if (providersWithoutApiKeys.includes(providerName?.toLowerCase())) {
 723 | 		return true; // Indicate key status is effectively "OK"
 724 | 	}
 725 | 
 726 | 	// Claude Code doesn't require an API key
 727 | 	if (providerName?.toLowerCase() === 'claude-code') {
 728 | 		return true; // No API key needed
 729 | 	}
 730 | 
 731 | 	const keyMap = {
 732 | 		openai: 'OPENAI_API_KEY',
 733 | 		anthropic: 'ANTHROPIC_API_KEY',
 734 | 		google: 'GOOGLE_API_KEY',
 735 | 		perplexity: 'PERPLEXITY_API_KEY',
 736 | 		mistral: 'MISTRAL_API_KEY',
 737 | 		azure: 'AZURE_OPENAI_API_KEY',
 738 | 		openrouter: 'OPENROUTER_API_KEY',
 739 | 		xai: 'XAI_API_KEY',
 740 | 		groq: 'GROQ_API_KEY',
 741 | 		vertex: 'GOOGLE_API_KEY', // Vertex uses the same key as Google
 742 | 		'claude-code': 'CLAUDE_CODE_API_KEY', // Not actually used, but included for consistency
 743 | 		bedrock: 'AWS_ACCESS_KEY_ID' // Bedrock uses AWS credentials
 744 | 		// Add other providers as needed
 745 | 	};
 746 | 
 747 | 	const providerKey = providerName?.toLowerCase();
 748 | 	if (!providerKey || !keyMap[providerKey]) {
 749 | 		log('warn', `Unknown provider name: ${providerName} in isApiKeySet check.`);
 750 | 		return false;
 751 | 	}
 752 | 
 753 | 	const envVarName = keyMap[providerKey];
 754 | 	const apiKeyValue = resolveEnvVariable(envVarName, session, projectRoot);
 755 | 
 756 | 	// Check if the key exists, is not empty, and is not a placeholder
 757 | 	return (
 758 | 		apiKeyValue &&
 759 | 		apiKeyValue.trim() !== '' &&
 760 | 		!/YOUR_.*_API_KEY_HERE/.test(apiKeyValue) && // General placeholder check
 761 | 		!apiKeyValue.includes('KEY_HERE')
 762 | 	); // Another common placeholder pattern
 763 | }
 764 | 
 765 | /**
 766 |  * Checks the API key status within .cursor/mcp.json for a given provider.
 767 |  * Reads the mcp.json file, finds the taskmaster-ai server config, and checks the relevant env var.
 768 |  * @param {string} providerName The name of the provider.
 769 |  * @param {string|null} projectRoot - Optional explicit path to the project root.
 770 |  * @returns {boolean} True if the key exists and is not a placeholder, false otherwise.
 771 |  */
 772 | function getMcpApiKeyStatus(providerName, projectRoot = null) {
 773 | 	const rootDir = projectRoot || findProjectRoot(); // Use existing root finding
 774 | 	if (!rootDir) {
 775 | 		console.warn(
 776 | 			chalk.yellow('Warning: Could not find project root to check mcp.json.')
 777 | 		);
 778 | 		return false; // Cannot check without root
 779 | 	}
 780 | 	const mcpConfigPath = path.join(rootDir, '.cursor', 'mcp.json');
 781 | 
 782 | 	if (!fs.existsSync(mcpConfigPath)) {
 783 | 		// console.warn(chalk.yellow('Warning: .cursor/mcp.json not found.'));
 784 | 		return false; // File doesn't exist
 785 | 	}
 786 | 
 787 | 	try {
 788 | 		const mcpConfigRaw = fs.readFileSync(mcpConfigPath, 'utf-8');
 789 | 		const mcpConfig = JSON.parse(mcpConfigRaw);
 790 | 
 791 | 		const mcpEnv =
 792 | 			mcpConfig?.mcpServers?.['task-master-ai']?.env ||
 793 | 			mcpConfig?.mcpServers?.['taskmaster-ai']?.env;
 794 | 		if (!mcpEnv) {
 795 | 			return false;
 796 | 		}
 797 | 
 798 | 		let apiKeyToCheck = null;
 799 | 		let placeholderValue = null;
 800 | 
 801 | 		switch (providerName) {
 802 | 			case 'anthropic':
 803 | 				apiKeyToCheck = mcpEnv.ANTHROPIC_API_KEY;
 804 | 				placeholderValue = 'YOUR_ANTHROPIC_API_KEY_HERE';
 805 | 				break;
 806 | 			case 'openai':
 807 | 				apiKeyToCheck = mcpEnv.OPENAI_API_KEY;
 808 | 				placeholderValue = 'YOUR_OPENAI_API_KEY_HERE'; // Assuming placeholder matches OPENAI
 809 | 				break;
 810 | 			case 'openrouter':
 811 | 				apiKeyToCheck = mcpEnv.OPENROUTER_API_KEY;
 812 | 				placeholderValue = 'YOUR_OPENROUTER_API_KEY_HERE';
 813 | 				break;
 814 | 			case 'google':
 815 | 				apiKeyToCheck = mcpEnv.GOOGLE_API_KEY;
 816 | 				placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE';
 817 | 				break;
 818 | 			case 'perplexity':
 819 | 				apiKeyToCheck = mcpEnv.PERPLEXITY_API_KEY;
 820 | 				placeholderValue = 'YOUR_PERPLEXITY_API_KEY_HERE';
 821 | 				break;
 822 | 			case 'xai':
 823 | 				apiKeyToCheck = mcpEnv.XAI_API_KEY;
 824 | 				placeholderValue = 'YOUR_XAI_API_KEY_HERE';
 825 | 				break;
 826 | 			case 'groq':
 827 | 				apiKeyToCheck = mcpEnv.GROQ_API_KEY;
 828 | 				placeholderValue = 'YOUR_GROQ_API_KEY_HERE';
 829 | 				break;
 830 | 			case 'ollama':
 831 | 				return true; // No key needed
 832 | 			case 'claude-code':
 833 | 				return true; // No key needed
 834 | 			case 'mistral':
 835 | 				apiKeyToCheck = mcpEnv.MISTRAL_API_KEY;
 836 | 				placeholderValue = 'YOUR_MISTRAL_API_KEY_HERE';
 837 | 				break;
 838 | 			case 'azure':
 839 | 				apiKeyToCheck = mcpEnv.AZURE_OPENAI_API_KEY;
 840 | 				placeholderValue = 'YOUR_AZURE_OPENAI_API_KEY_HERE';
 841 | 				break;
 842 | 			case 'vertex':
 843 | 				apiKeyToCheck = mcpEnv.GOOGLE_API_KEY; // Vertex uses Google API key
 844 | 				placeholderValue = 'YOUR_GOOGLE_API_KEY_HERE';
 845 | 				break;
 846 | 			case 'bedrock':
 847 | 				apiKeyToCheck = mcpEnv.AWS_ACCESS_KEY_ID; // Bedrock uses AWS credentials
 848 | 				placeholderValue = 'YOUR_AWS_ACCESS_KEY_ID_HERE';
 849 | 				break;
 850 | 			default:
 851 | 				return false; // Unknown provider
 852 | 		}
 853 | 
 854 | 		return !!apiKeyToCheck && !/KEY_HERE$/.test(apiKeyToCheck);
 855 | 	} catch (error) {
 856 | 		console.error(
 857 | 			chalk.red(`Error reading or parsing .cursor/mcp.json: ${error.message}`)
 858 | 		);
 859 | 		return false;
 860 | 	}
 861 | }
 862 | 
 863 | /**
 864 |  * Gets a list of available models based on the MODEL_MAP.
 865 |  * @returns {Array<{id: string, name: string, provider: string, swe_score: number|null, cost_per_1m_tokens: {input: number|null, output: number|null}|null, allowed_roles: string[]}>}
 866 |  */
 867 | function getAvailableModels() {
 868 | 	const available = [];
 869 | 	for (const [provider, models] of Object.entries(MODEL_MAP)) {
 870 | 		if (models.length > 0) {
 871 | 			models
 872 | 				.filter((modelObj) => Boolean(modelObj.supported))
 873 | 				.forEach((modelObj) => {
 874 | 					// Basic name generation - can be improved
 875 | 					const modelId = modelObj.id;
 876 | 					const sweScore = modelObj.swe_score;
 877 | 					const cost = modelObj.cost_per_1m_tokens;
 878 | 					const allowedRoles = modelObj.allowed_roles || ['main', 'fallback'];
 879 | 					const nameParts = modelId
 880 | 						.split('-')
 881 | 						.map((p) => p.charAt(0).toUpperCase() + p.slice(1));
 882 | 					// Handle specific known names better if needed
 883 | 					let name = nameParts.join(' ');
 884 | 					if (modelId === 'claude-3.5-sonnet-20240620')
 885 | 						name = 'Claude 3.5 Sonnet';
 886 | 					if (modelId === 'claude-3-7-sonnet-20250219')
 887 | 						name = 'Claude 3.7 Sonnet';
 888 | 					if (modelId === 'gpt-4o') name = 'GPT-4o';
 889 | 					if (modelId === 'gpt-4-turbo') name = 'GPT-4 Turbo';
 890 | 					if (modelId === 'sonar-pro') name = 'Perplexity Sonar Pro';
 891 | 					if (modelId === 'sonar-mini') name = 'Perplexity Sonar Mini';
 892 | 
 893 | 					available.push({
 894 | 						id: modelId,
 895 | 						name: name,
 896 | 						provider: provider,
 897 | 						swe_score: sweScore,
 898 | 						cost_per_1m_tokens: cost,
 899 | 						allowed_roles: allowedRoles,
 900 | 						max_tokens: modelObj.max_tokens
 901 | 					});
 902 | 				});
 903 | 		} else {
 904 | 			// For providers with empty lists (like ollama), maybe add a placeholder or skip
 905 | 			available.push({
 906 | 				id: `[${provider}-any]`,
 907 | 				name: `Any (${provider})`,
 908 | 				provider: provider
 909 | 			});
 910 | 		}
 911 | 	}
 912 | 	return available;
 913 | }
 914 | 
 915 | /**
 916 |  * Writes the configuration object to the file.
 917 |  * @param {Object} config The configuration object to write.
 918 |  * @param {string|null} explicitRoot - Optional explicit path to the project root.
 919 |  * @returns {boolean} True if successful, false otherwise.
 920 |  */
 921 | function writeConfig(config, explicitRoot = null) {
 922 | 	// ---> Determine root path reliably <---
 923 | 	let rootPath = explicitRoot;
 924 | 	if (explicitRoot === null || explicitRoot === undefined) {
 925 | 		// Logic matching _loadAndValidateConfig
 926 | 		const foundRoot = findProjectRoot(); // *** Explicitly call findProjectRoot ***
 927 | 		if (!foundRoot) {
 928 | 			console.error(
 929 | 				chalk.red(
 930 | 					'Error: Could not determine project root. Configuration not saved.'
 931 | 				)
 932 | 			);
 933 | 			return false;
 934 | 		}
 935 | 		rootPath = foundRoot;
 936 | 	}
 937 | 	// ---> End determine root path logic <---
 938 | 
 939 | 	// Use new config location: .taskmaster/config.json
 940 | 	const taskmasterDir = path.join(rootPath, '.taskmaster');
 941 | 	const configPath = path.join(taskmasterDir, 'config.json');
 942 | 
 943 | 	try {
 944 | 		// Ensure .taskmaster directory exists
 945 | 		if (!fs.existsSync(taskmasterDir)) {
 946 | 			fs.mkdirSync(taskmasterDir, { recursive: true });
 947 | 		}
 948 | 
 949 | 		fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
 950 | 		loadedConfig = config; // Update the cache after successful write
 951 | 		return true;
 952 | 	} catch (error) {
 953 | 		console.error(
 954 | 			chalk.red(
 955 | 				`Error writing configuration to ${configPath}: ${error.message}`
 956 | 			)
 957 | 		);
 958 | 		return false;
 959 | 	}
 960 | }
 961 | 
 962 | /**
 963 |  * Checks if a configuration file exists at the project root (new or legacy location)
 964 |  * @param {string|null} explicitRoot - Optional explicit path to the project root
 965 |  * @returns {boolean} True if the file exists, false otherwise
 966 |  */
 967 | function isConfigFilePresent(explicitRoot = null) {
 968 | 	return findConfigPath(null, { projectRoot: explicitRoot }) !== null;
 969 | }
 970 | 
 971 | /**
 972 |  * Gets the user ID from the configuration.
 973 |  * @param {string|null} explicitRoot - Optional explicit path to the project root.
 974 |  * @returns {string|null} The user ID or null if not found.
 975 |  */
 976 | function getUserId(explicitRoot = null) {
 977 | 	const config = getConfig(explicitRoot);
 978 | 	if (!config.global) {
 979 | 		config.global = {}; // Ensure global object exists
 980 | 	}
 981 | 	if (!config.global.userId) {
 982 | 		config.global.userId = '1234567890';
 983 | 		// Attempt to write the updated config.
 984 | 		// It's important that writeConfig correctly resolves the path
 985 | 		// using explicitRoot, similar to how getConfig does.
 986 | 		const success = writeConfig(config, explicitRoot);
 987 | 		if (!success) {
 988 | 			// Log an error or handle the failure to write,
 989 | 			// though for now, we'll proceed with the in-memory default.
 990 | 			log(
 991 | 				'warning',
 992 | 				'Failed to write updated configuration with new userId. Please let the developers know.'
 993 | 			);
 994 | 		}
 995 | 	}
 996 | 	return config.global.userId;
 997 | }
 998 | 
 999 | /**
1000 |  * Gets a list of all known provider names (both validated and custom).
1001 |  * @returns {string[]} An array of all provider names.
1002 |  */
1003 | function getAllProviders() {
1004 | 	return ALL_PROVIDERS;
1005 | }
1006 | 
1007 | function getBaseUrlForRole(role, explicitRoot = null) {
1008 | 	const roleConfig = getModelConfigForRole(role, explicitRoot);
1009 | 	if (roleConfig && typeof roleConfig.baseURL === 'string') {
1010 | 		return roleConfig.baseURL;
1011 | 	}
1012 | 	const provider = roleConfig?.provider;
1013 | 	if (provider) {
1014 | 		const envVarName = `${provider.toUpperCase()}_BASE_URL`;
1015 | 		return resolveEnvVariable(envVarName, null, explicitRoot);
1016 | 	}
1017 | 	return undefined;
1018 | }
1019 | 
1020 | // Export the providers without API keys array for use in other modules
1021 | export const providersWithoutApiKeys = [
1022 | 	CUSTOM_PROVIDERS.OLLAMA,
1023 | 	CUSTOM_PROVIDERS.BEDROCK,
1024 | 	CUSTOM_PROVIDERS.GEMINI_CLI,
1025 | 	CUSTOM_PROVIDERS.GROK_CLI,
1026 | 	CUSTOM_PROVIDERS.MCP
1027 | ];
1028 | 
1029 | export {
1030 | 	// Core config access
1031 | 	getConfig,
1032 | 	writeConfig,
1033 | 	ConfigurationError,
1034 | 	isConfigFilePresent,
1035 | 	// Claude Code settings
1036 | 	getClaudeCodeSettings,
1037 | 	getClaudeCodeSettingsForCommand,
1038 | 	// Grok CLI settings
1039 | 	getGrokCliSettings,
1040 | 	getGrokCliSettingsForCommand,
1041 | 	// Validation
1042 | 	validateProvider,
1043 | 	validateProviderModelCombination,
1044 | 	validateClaudeCodeSettings,
1045 | 	VALIDATED_PROVIDERS,
1046 | 	CUSTOM_PROVIDERS,
1047 | 	ALL_PROVIDERS,
1048 | 	MODEL_MAP,
1049 | 	getAvailableModels,
1050 | 	// Role-specific getters (No env var overrides)
1051 | 	getMainProvider,
1052 | 	getMainModelId,
1053 | 	getMainMaxTokens,
1054 | 	getMainTemperature,
1055 | 	getResearchProvider,
1056 | 	getResearchModelId,
1057 | 	getResearchMaxTokens,
1058 | 	getResearchTemperature,
1059 | 	hasCodebaseAnalysis,
1060 | 	getFallbackProvider,
1061 | 	getFallbackModelId,
1062 | 	getFallbackMaxTokens,
1063 | 	getFallbackTemperature,
1064 | 	getBaseUrlForRole,
1065 | 	// Global setting getters (No env var overrides)
1066 | 	getLogLevel,
1067 | 	getDebugFlag,
1068 | 	getDefaultNumTasks,
1069 | 	getDefaultSubtasks,
1070 | 	getDefaultPriority,
1071 | 	getProjectName,
1072 | 	getOllamaBaseURL,
1073 | 	getAzureBaseURL,
1074 | 	getBedrockBaseURL,
1075 | 	getResponseLanguage,
1076 | 	getCodebaseAnalysisEnabled,
1077 | 	isCodebaseAnalysisEnabled,
1078 | 	getParametersForRole,
1079 | 	getUserId,
1080 | 	// API Key Checkers (still relevant)
1081 | 	isApiKeySet,
1082 | 	getMcpApiKeyStatus,
1083 | 	// ADD: Function to get all provider names
1084 | 	getAllProviders,
1085 | 	getVertexProjectId,
1086 | 	getVertexLocation
1087 | };
1088 | 
```

--------------------------------------------------------------------------------
/scripts/modules/task-manager/list-tasks.js:
--------------------------------------------------------------------------------

```javascript
   1 | import chalk from 'chalk';
   2 | import boxen from 'boxen';
   3 | import Table from 'cli-table3';
   4 | 
   5 | import {
   6 | 	log,
   7 | 	readJSON,
   8 | 	truncate,
   9 | 	readComplexityReport,
  10 | 	addComplexityToTask
  11 | } from '../utils.js';
  12 | import findNextTask from './find-next-task.js';
  13 | 
  14 | import {
  15 | 	displayBanner,
  16 | 	getStatusWithColor,
  17 | 	formatDependenciesWithStatus,
  18 | 	getComplexityWithColor,
  19 | 	createProgressBar
  20 | } from '../ui.js';
  21 | 
  22 | /**
  23 |  * List all tasks
  24 |  * @param {string} tasksPath - Path to the tasks.json file
  25 |  * @param {string} statusFilter - Filter by status (single status or comma-separated list, e.g., 'pending' or 'blocked,deferred')
  26 |  * @param {string} reportPath - Path to the complexity report
  27 |  * @param {boolean} withSubtasks - Whether to show subtasks
  28 |  * @param {string} outputFormat - Output format (text or json)
  29 |  * @param {Object} context - Context object (required)
  30 |  * @param {string} context.projectRoot - Project root path
  31 |  * @param {string} context.tag - Tag for the task
  32 |  * @returns {Object} - Task list result for json format
  33 |  */
  34 | function listTasks(
  35 | 	tasksPath,
  36 | 	statusFilter,
  37 | 	reportPath = null,
  38 | 	withSubtasks = false,
  39 | 	outputFormat = 'text',
  40 | 	context = {}
  41 | ) {
  42 | 	const { projectRoot, tag } = context;
  43 | 	try {
  44 | 		// Extract projectRoot from context if provided
  45 | 		const data = readJSON(tasksPath, projectRoot, tag); // Pass projectRoot to readJSON
  46 | 		if (!data || !data.tasks) {
  47 | 			throw new Error(`No valid tasks found in ${tasksPath}`);
  48 | 		}
  49 | 
  50 | 		// Add complexity scores to tasks if report exists
  51 | 		// `reportPath` is already tag-aware (resolved at the CLI boundary).
  52 | 		const complexityReport = readComplexityReport(reportPath);
  53 | 		// Apply complexity scores to tasks
  54 | 		if (complexityReport && complexityReport.complexityAnalysis) {
  55 | 			data.tasks.forEach((task) => addComplexityToTask(task, complexityReport));
  56 | 		}
  57 | 
  58 | 		// Filter tasks by status if specified - now supports comma-separated statuses
  59 | 		let filteredTasks;
  60 | 		if (statusFilter && statusFilter.toLowerCase() !== 'all') {
  61 | 			// Handle comma-separated statuses
  62 | 			const allowedStatuses = statusFilter
  63 | 				.split(',')
  64 | 				.map((s) => s.trim().toLowerCase())
  65 | 				.filter((s) => s.length > 0); // Remove empty strings
  66 | 
  67 | 			filteredTasks = data.tasks.filter(
  68 | 				(task) =>
  69 | 					task.status && allowedStatuses.includes(task.status.toLowerCase())
  70 | 			);
  71 | 		} else {
  72 | 			// Default to all tasks if no filter or filter is 'all'
  73 | 			filteredTasks = data.tasks;
  74 | 		}
  75 | 
  76 | 		// Calculate completion statistics
  77 | 		const totalTasks = data.tasks.length;
  78 | 		const completedTasks = data.tasks.filter(
  79 | 			(task) => task.status === 'done' || task.status === 'completed'
  80 | 		).length;
  81 | 		const completionPercentage =
  82 | 			totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
  83 | 
  84 | 		// Count statuses for tasks
  85 | 		const doneCount = completedTasks;
  86 | 		const inProgressCount = data.tasks.filter(
  87 | 			(task) => task.status === 'in-progress'
  88 | 		).length;
  89 | 		const pendingCount = data.tasks.filter(
  90 | 			(task) => task.status === 'pending'
  91 | 		).length;
  92 | 		const blockedCount = data.tasks.filter(
  93 | 			(task) => task.status === 'blocked'
  94 | 		).length;
  95 | 		const deferredCount = data.tasks.filter(
  96 | 			(task) => task.status === 'deferred'
  97 | 		).length;
  98 | 		const cancelledCount = data.tasks.filter(
  99 | 			(task) => task.status === 'cancelled'
 100 | 		).length;
 101 | 		const reviewCount = data.tasks.filter(
 102 | 			(task) => task.status === 'review'
 103 | 		).length;
 104 | 
 105 | 		// Count subtasks and their statuses
 106 | 		let totalSubtasks = 0;
 107 | 		let completedSubtasks = 0;
 108 | 		let inProgressSubtasks = 0;
 109 | 		let pendingSubtasks = 0;
 110 | 		let blockedSubtasks = 0;
 111 | 		let deferredSubtasks = 0;
 112 | 		let cancelledSubtasks = 0;
 113 | 		let reviewSubtasks = 0;
 114 | 
 115 | 		data.tasks.forEach((task) => {
 116 | 			if (task.subtasks && task.subtasks.length > 0) {
 117 | 				totalSubtasks += task.subtasks.length;
 118 | 				completedSubtasks += task.subtasks.filter(
 119 | 					(st) => st.status === 'done' || st.status === 'completed'
 120 | 				).length;
 121 | 				inProgressSubtasks += task.subtasks.filter(
 122 | 					(st) => st.status === 'in-progress'
 123 | 				).length;
 124 | 				pendingSubtasks += task.subtasks.filter(
 125 | 					(st) => st.status === 'pending'
 126 | 				).length;
 127 | 				blockedSubtasks += task.subtasks.filter(
 128 | 					(st) => st.status === 'blocked'
 129 | 				).length;
 130 | 				deferredSubtasks += task.subtasks.filter(
 131 | 					(st) => st.status === 'deferred'
 132 | 				).length;
 133 | 				cancelledSubtasks += task.subtasks.filter(
 134 | 					(st) => st.status === 'cancelled'
 135 | 				).length;
 136 | 				reviewSubtasks += task.subtasks.filter(
 137 | 					(st) => st.status === 'review'
 138 | 				).length;
 139 | 			}
 140 | 		});
 141 | 
 142 | 		const subtaskCompletionPercentage =
 143 | 			totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
 144 | 
 145 | 		// Calculate dependency statistics (moved up to be available for all output formats)
 146 | 		const completedTaskIds = new Set(
 147 | 			data.tasks
 148 | 				.filter((t) => t.status === 'done' || t.status === 'completed')
 149 | 				.map((t) => t.id)
 150 | 		);
 151 | 
 152 | 		const tasksWithNoDeps = data.tasks.filter(
 153 | 			(t) =>
 154 | 				t.status !== 'done' &&
 155 | 				t.status !== 'completed' &&
 156 | 				(!t.dependencies || t.dependencies.length === 0)
 157 | 		).length;
 158 | 
 159 | 		const tasksWithAllDepsSatisfied = data.tasks.filter(
 160 | 			(t) =>
 161 | 				t.status !== 'done' &&
 162 | 				t.status !== 'completed' &&
 163 | 				t.dependencies &&
 164 | 				t.dependencies.length > 0 &&
 165 | 				t.dependencies.every((depId) => completedTaskIds.has(depId))
 166 | 		).length;
 167 | 
 168 | 		const tasksWithUnsatisfiedDeps = data.tasks.filter(
 169 | 			(t) =>
 170 | 				t.status !== 'done' &&
 171 | 				t.status !== 'completed' &&
 172 | 				t.dependencies &&
 173 | 				t.dependencies.length > 0 &&
 174 | 				!t.dependencies.every((depId) => completedTaskIds.has(depId))
 175 | 		).length;
 176 | 
 177 | 		// Calculate total tasks ready to work on (no deps + satisfied deps)
 178 | 		const tasksReadyToWork = tasksWithNoDeps + tasksWithAllDepsSatisfied;
 179 | 
 180 | 		// Calculate most depended-on tasks
 181 | 		const dependencyCount = {};
 182 | 		data.tasks.forEach((task) => {
 183 | 			if (task.dependencies && task.dependencies.length > 0) {
 184 | 				task.dependencies.forEach((depId) => {
 185 | 					dependencyCount[depId] = (dependencyCount[depId] || 0) + 1;
 186 | 				});
 187 | 			}
 188 | 		});
 189 | 
 190 | 		// Find the most depended-on task
 191 | 		let mostDependedOnTaskId = null;
 192 | 		let maxDependents = 0;
 193 | 
 194 | 		for (const [taskId, count] of Object.entries(dependencyCount)) {
 195 | 			if (count > maxDependents) {
 196 | 				maxDependents = count;
 197 | 				mostDependedOnTaskId = parseInt(taskId);
 198 | 			}
 199 | 		}
 200 | 
 201 | 		// Get the most depended-on task
 202 | 		const mostDependedOnTask =
 203 | 			mostDependedOnTaskId !== null
 204 | 				? data.tasks.find((t) => t.id === mostDependedOnTaskId)
 205 | 				: null;
 206 | 
 207 | 		// Calculate average dependencies per task
 208 | 		const totalDependencies = data.tasks.reduce(
 209 | 			(sum, task) => sum + (task.dependencies ? task.dependencies.length : 0),
 210 | 			0
 211 | 		);
 212 | 		const avgDependenciesPerTask = totalDependencies / data.tasks.length;
 213 | 
 214 | 		// Find next task to work on, passing the complexity report
 215 | 		const nextItem = findNextTask(data.tasks, complexityReport);
 216 | 
 217 | 		// For JSON output, return structured data
 218 | 		if (outputFormat === 'json') {
 219 | 			// *** Modification: Remove 'details' field for JSON output ***
 220 | 			const tasksWithoutDetails = filteredTasks.map((task) => {
 221 | 				// <-- USES filteredTasks!
 222 | 				// Omit 'details' from the parent task
 223 | 				const { details, ...taskRest } = task;
 224 | 
 225 | 				// If subtasks exist, omit 'details' from them too
 226 | 				if (taskRest.subtasks && Array.isArray(taskRest.subtasks)) {
 227 | 					taskRest.subtasks = taskRest.subtasks.map((subtask) => {
 228 | 						const { details: subtaskDetails, ...subtaskRest } = subtask;
 229 | 						return subtaskRest;
 230 | 					});
 231 | 				}
 232 | 				return taskRest;
 233 | 			});
 234 | 			// *** End of Modification ***
 235 | 
 236 | 			return {
 237 | 				tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED
 238 | 				filter: statusFilter || 'all', // Return the actual filter used
 239 | 				stats: {
 240 | 					total: totalTasks,
 241 | 					completed: doneCount,
 242 | 					inProgress: inProgressCount,
 243 | 					pending: pendingCount,
 244 | 					blocked: blockedCount,
 245 | 					deferred: deferredCount,
 246 | 					cancelled: cancelledCount,
 247 | 					review: reviewCount,
 248 | 					completionPercentage,
 249 | 					subtasks: {
 250 | 						total: totalSubtasks,
 251 | 						completed: completedSubtasks,
 252 | 						inProgress: inProgressSubtasks,
 253 | 						pending: pendingSubtasks,
 254 | 						blocked: blockedSubtasks,
 255 | 						deferred: deferredSubtasks,
 256 | 						cancelled: cancelledSubtasks,
 257 | 						completionPercentage: subtaskCompletionPercentage
 258 | 					}
 259 | 				}
 260 | 			};
 261 | 		}
 262 | 
 263 | 		// For markdown-readme output, return formatted markdown
 264 | 		if (outputFormat === 'markdown-readme') {
 265 | 			return generateMarkdownOutput(data, filteredTasks, {
 266 | 				totalTasks,
 267 | 				completedTasks,
 268 | 				completionPercentage,
 269 | 				doneCount,
 270 | 				inProgressCount,
 271 | 				pendingCount,
 272 | 				blockedCount,
 273 | 				deferredCount,
 274 | 				cancelledCount,
 275 | 				totalSubtasks,
 276 | 				completedSubtasks,
 277 | 				subtaskCompletionPercentage,
 278 | 				inProgressSubtasks,
 279 | 				pendingSubtasks,
 280 | 				blockedSubtasks,
 281 | 				deferredSubtasks,
 282 | 				cancelledSubtasks,
 283 | 				reviewSubtasks,
 284 | 				tasksWithNoDeps,
 285 | 				tasksReadyToWork,
 286 | 				tasksWithUnsatisfiedDeps,
 287 | 				mostDependedOnTask,
 288 | 				mostDependedOnTaskId,
 289 | 				maxDependents,
 290 | 				avgDependenciesPerTask,
 291 | 				complexityReport,
 292 | 				withSubtasks,
 293 | 				nextItem
 294 | 			});
 295 | 		}
 296 | 
 297 | 		// For compact output, return minimal one-line format
 298 | 		if (outputFormat === 'compact') {
 299 | 			return renderCompactOutput(filteredTasks, withSubtasks);
 300 | 		}
 301 | 
 302 | 		// ... existing code for text output ...
 303 | 
 304 | 		// Calculate status breakdowns as percentages of total
 305 | 		const taskStatusBreakdown = {
 306 | 			'in-progress': totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0,
 307 | 			pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0,
 308 | 			blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0,
 309 | 			deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0,
 310 | 			cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0,
 311 | 			review: totalTasks > 0 ? (reviewCount / totalTasks) * 100 : 0
 312 | 		};
 313 | 
 314 | 		const subtaskStatusBreakdown = {
 315 | 			'in-progress':
 316 | 				totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0,
 317 | 			pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0,
 318 | 			blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0,
 319 | 			deferred:
 320 | 				totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0,
 321 | 			cancelled:
 322 | 				totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0,
 323 | 			review: totalSubtasks > 0 ? (reviewSubtasks / totalSubtasks) * 100 : 0
 324 | 		};
 325 | 
 326 | 		// Create progress bars with status breakdowns
 327 | 		const taskProgressBar = createProgressBar(
 328 | 			completionPercentage,
 329 | 			30,
 330 | 			taskStatusBreakdown
 331 | 		);
 332 | 		const subtaskProgressBar = createProgressBar(
 333 | 			subtaskCompletionPercentage,
 334 | 			30,
 335 | 			subtaskStatusBreakdown
 336 | 		);
 337 | 
 338 | 		// Get terminal width - more reliable method
 339 | 		let terminalWidth;
 340 | 		try {
 341 | 			// Try to get the actual terminal columns
 342 | 			terminalWidth = process.stdout.columns;
 343 | 		} catch (e) {
 344 | 			// Fallback if columns cannot be determined
 345 | 			log('debug', 'Could not determine terminal width, using default');
 346 | 		}
 347 | 		// Ensure we have a reasonable default if detection fails
 348 | 		terminalWidth = terminalWidth || 80;
 349 | 
 350 | 		// Ensure terminal width is at least a minimum value to prevent layout issues
 351 | 		terminalWidth = Math.max(terminalWidth, 80);
 352 | 
 353 | 		// Create dashboard content
 354 | 		const projectDashboardContent =
 355 | 			chalk.white.bold('Project Dashboard') +
 356 | 			'\n' +
 357 | 			`Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` +
 358 | 			`Done: ${chalk.green(doneCount)}  In Progress: ${chalk.blue(inProgressCount)}  Pending: ${chalk.yellow(pendingCount)}  Blocked: ${chalk.red(blockedCount)}  Deferred: ${chalk.gray(deferredCount)}  Cancelled: ${chalk.gray(cancelledCount)}\n\n` +
 359 | 			`Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` +
 360 | 			`Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks}  In Progress: ${chalk.blue(inProgressSubtasks)}  Pending: ${chalk.yellow(pendingSubtasks)}  Blocked: ${chalk.red(blockedSubtasks)}  Deferred: ${chalk.gray(deferredSubtasks)}  Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` +
 361 | 			chalk.cyan.bold('Priority Breakdown:') +
 362 | 			'\n' +
 363 | 			`${chalk.red('•')} ${chalk.white('High priority:')} ${data.tasks.filter((t) => t.priority === 'high').length}\n` +
 364 | 			`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${data.tasks.filter((t) => t.priority === 'medium').length}\n` +
 365 | 			`${chalk.green('•')} ${chalk.white('Low priority:')} ${data.tasks.filter((t) => t.priority === 'low').length}`;
 366 | 
 367 | 		const dependencyDashboardContent =
 368 | 			chalk.white.bold('Dependency Status & Next Task') +
 369 | 			'\n' +
 370 | 			chalk.cyan.bold('Dependency Metrics:') +
 371 | 			'\n' +
 372 | 			`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${tasksWithNoDeps}\n` +
 373 | 			`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${tasksReadyToWork}\n` +
 374 | 			`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${tasksWithUnsatisfiedDeps}\n` +
 375 | 			`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray('None')}\n` +
 376 | 			`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${avgDependenciesPerTask.toFixed(1)}\n\n` +
 377 | 			chalk.cyan.bold('Next Task to Work On:') +
 378 | 			'\n' +
 379 | 			`ID: ${chalk.cyan(nextItem ? nextItem.id : 'N/A')} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow('No task available')}
 380 | ` +
 381 | 			`Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''}  Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ''}
 382 | ` +
 383 | 			`Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray('N/A')}`;
 384 | 
 385 | 		// Calculate width for side-by-side display
 386 | 		// Box borders, padding take approximately 4 chars on each side
 387 | 		const minDashboardWidth = 50; // Minimum width for dashboard
 388 | 		const minDependencyWidth = 50; // Minimum width for dependency dashboard
 389 | 		const totalMinWidth = minDashboardWidth + minDependencyWidth + 4; // Extra 4 chars for spacing
 390 | 
 391 | 		// If terminal is wide enough, show boxes side by side with responsive widths
 392 | 		if (terminalWidth >= totalMinWidth) {
 393 | 			// Calculate widths proportionally for each box - use exact 50% width each
 394 | 			const availableWidth = terminalWidth;
 395 | 			const halfWidth = Math.floor(availableWidth / 2);
 396 | 
 397 | 			// Account for border characters (2 chars on each side)
 398 | 			const boxContentWidth = halfWidth - 4;
 399 | 
 400 | 			// Create boxen options with precise widths
 401 | 			const dashboardBox = boxen(projectDashboardContent, {
 402 | 				padding: 1,
 403 | 				borderColor: 'blue',
 404 | 				borderStyle: 'round',
 405 | 				width: boxContentWidth,
 406 | 				dimBorder: false
 407 | 			});
 408 | 
 409 | 			const dependencyBox = boxen(dependencyDashboardContent, {
 410 | 				padding: 1,
 411 | 				borderColor: 'magenta',
 412 | 				borderStyle: 'round',
 413 | 				width: boxContentWidth,
 414 | 				dimBorder: false
 415 | 			});
 416 | 
 417 | 			// Create a better side-by-side layout with exact spacing
 418 | 			const dashboardLines = dashboardBox.split('\n');
 419 | 			const dependencyLines = dependencyBox.split('\n');
 420 | 
 421 | 			// Make sure both boxes have the same height
 422 | 			const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
 423 | 
 424 | 			// For each line of output, pad the dashboard line to exactly halfWidth chars
 425 | 			// This ensures the dependency box starts at exactly the right position
 426 | 			const combinedLines = [];
 427 | 			for (let i = 0; i < maxHeight; i++) {
 428 | 				// Get the dashboard line (or empty string if we've run out of lines)
 429 | 				const dashLine = i < dashboardLines.length ? dashboardLines[i] : '';
 430 | 				// Get the dependency line (or empty string if we've run out of lines)
 431 | 				const depLine = i < dependencyLines.length ? dependencyLines[i] : '';
 432 | 
 433 | 				// Remove any trailing spaces from dashLine before padding to exact width
 434 | 				const trimmedDashLine = dashLine.trimEnd();
 435 | 				// Pad the dashboard line to exactly halfWidth chars with no extra spaces
 436 | 				const paddedDashLine = trimmedDashLine.padEnd(halfWidth, ' ');
 437 | 
 438 | 				// Join the lines with no space in between
 439 | 				combinedLines.push(paddedDashLine + depLine);
 440 | 			}
 441 | 
 442 | 			// Join all lines and output
 443 | 			console.log(combinedLines.join('\n'));
 444 | 		} else {
 445 | 			// Terminal too narrow, show boxes stacked vertically
 446 | 			const dashboardBox = boxen(projectDashboardContent, {
 447 | 				padding: 1,
 448 | 				borderColor: 'blue',
 449 | 				borderStyle: 'round',
 450 | 				margin: { top: 0, bottom: 1 }
 451 | 			});
 452 | 
 453 | 			const dependencyBox = boxen(dependencyDashboardContent, {
 454 | 				padding: 1,
 455 | 				borderColor: 'magenta',
 456 | 				borderStyle: 'round',
 457 | 				margin: { top: 0, bottom: 1 }
 458 | 			});
 459 | 
 460 | 			// Display stacked vertically
 461 | 			console.log(dashboardBox);
 462 | 			console.log(dependencyBox);
 463 | 		}
 464 | 
 465 | 		if (filteredTasks.length === 0) {
 466 | 			console.log(
 467 | 				boxen(
 468 | 					statusFilter
 469 | 						? chalk.yellow(`No tasks with status '${statusFilter}' found`)
 470 | 						: chalk.yellow('No tasks found'),
 471 | 					{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
 472 | 				)
 473 | 			);
 474 | 			return;
 475 | 		}
 476 | 
 477 | 		// COMPLETELY REVISED TABLE APPROACH
 478 | 		// Define percentage-based column widths and calculate actual widths
 479 | 		// Adjust percentages based on content type and user requirements
 480 | 
 481 | 		// Adjust ID width if showing subtasks (subtask IDs are longer: e.g., "1.2")
 482 | 		const idWidthPct = withSubtasks ? 10 : 7;
 483 | 
 484 | 		// Calculate max status length to accommodate "in-progress"
 485 | 		const statusWidthPct = 15;
 486 | 
 487 | 		// Increase priority column width as requested
 488 | 		const priorityWidthPct = 12;
 489 | 
 490 | 		// Make dependencies column smaller as requested (-20%)
 491 | 		const depsWidthPct = 20;
 492 | 
 493 | 		const complexityWidthPct = 10;
 494 | 
 495 | 		// Calculate title/description width as remaining space (+20% from dependencies reduction)
 496 | 		const titleWidthPct =
 497 | 			100 -
 498 | 			idWidthPct -
 499 | 			statusWidthPct -
 500 | 			priorityWidthPct -
 501 | 			depsWidthPct -
 502 | 			complexityWidthPct;
 503 | 
 504 | 		// Allow 10 characters for borders and padding
 505 | 		const availableWidth = terminalWidth - 10;
 506 | 
 507 | 		// Calculate actual column widths based on percentages
 508 | 		const idWidth = Math.floor(availableWidth * (idWidthPct / 100));
 509 | 		const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100));
 510 | 		const priorityWidth = Math.floor(availableWidth * (priorityWidthPct / 100));
 511 | 		const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100));
 512 | 		const complexityWidth = Math.floor(
 513 | 			availableWidth * (complexityWidthPct / 100)
 514 | 		);
 515 | 		const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100));
 516 | 
 517 | 		// Create a table with correct borders and spacing
 518 | 		const table = new Table({
 519 | 			head: [
 520 | 				chalk.cyan.bold('ID'),
 521 | 				chalk.cyan.bold('Title'),
 522 | 				chalk.cyan.bold('Status'),
 523 | 				chalk.cyan.bold('Priority'),
 524 | 				chalk.cyan.bold('Dependencies'),
 525 | 				chalk.cyan.bold('Complexity')
 526 | 			],
 527 | 			colWidths: [
 528 | 				idWidth,
 529 | 				titleWidth,
 530 | 				statusWidth,
 531 | 				priorityWidth,
 532 | 				depsWidth,
 533 | 				complexityWidth // Added complexity column width
 534 | 			],
 535 | 			style: {
 536 | 				head: [], // No special styling for header
 537 | 				border: [], // No special styling for border
 538 | 				compact: false // Use default spacing
 539 | 			},
 540 | 			wordWrap: true,
 541 | 			wrapOnWordBoundary: true
 542 | 		});
 543 | 
 544 | 		// Process tasks for the table
 545 | 		filteredTasks.forEach((task) => {
 546 | 			// Format dependencies with status indicators (colored)
 547 | 			let depText = 'None';
 548 | 			if (task.dependencies && task.dependencies.length > 0) {
 549 | 				// Use the proper formatDependenciesWithStatus function for colored status
 550 | 				depText = formatDependenciesWithStatus(
 551 | 					task.dependencies,
 552 | 					data.tasks,
 553 | 					true,
 554 | 					complexityReport
 555 | 				);
 556 | 			} else {
 557 | 				depText = chalk.gray('None');
 558 | 			}
 559 | 
 560 | 			// Clean up any ANSI codes or confusing characters
 561 | 			const cleanTitle = task.title.replace(/\n/g, ' ');
 562 | 
 563 | 			// Get priority color
 564 | 			const priorityColor =
 565 | 				{
 566 | 					high: chalk.red,
 567 | 					medium: chalk.yellow,
 568 | 					low: chalk.gray
 569 | 				}[task.priority || 'medium'] || chalk.white;
 570 | 
 571 | 			// Format status
 572 | 			const status = getStatusWithColor(task.status, true);
 573 | 
 574 | 			// Add the row without truncating dependencies
 575 | 			table.push([
 576 | 				task.id.toString(),
 577 | 				truncate(cleanTitle, titleWidth - 3),
 578 | 				status,
 579 | 				priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)),
 580 | 				depText,
 581 | 				task.complexityScore
 582 | 					? getComplexityWithColor(task.complexityScore)
 583 | 					: chalk.gray('N/A')
 584 | 			]);
 585 | 
 586 | 			// Add subtasks if requested
 587 | 			if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
 588 | 				task.subtasks.forEach((subtask) => {
 589 | 					// Format subtask dependencies with status indicators
 590 | 					let subtaskDepText = 'None';
 591 | 					if (subtask.dependencies && subtask.dependencies.length > 0) {
 592 | 						// Handle both subtask-to-subtask and subtask-to-task dependencies
 593 | 						const formattedDeps = subtask.dependencies
 594 | 							.map((depId) => {
 595 | 								// Check if it's a dependency on another subtask
 596 | 								if (typeof depId === 'number' && depId < 100) {
 597 | 									const foundSubtask = task.subtasks.find(
 598 | 										(st) => st.id === depId
 599 | 									);
 600 | 									if (foundSubtask) {
 601 | 										const isDone =
 602 | 											foundSubtask.status === 'done' ||
 603 | 											foundSubtask.status === 'completed';
 604 | 										const isInProgress = foundSubtask.status === 'in-progress';
 605 | 
 606 | 										// Use consistent color formatting instead of emojis
 607 | 										if (isDone) {
 608 | 											return chalk.green.bold(`${task.id}.${depId}`);
 609 | 										} else if (isInProgress) {
 610 | 											return chalk.hex('#FFA500').bold(`${task.id}.${depId}`);
 611 | 										} else {
 612 | 											return chalk.red.bold(`${task.id}.${depId}`);
 613 | 										}
 614 | 									}
 615 | 								}
 616 | 								// Default to regular task dependency
 617 | 								const depTask = data.tasks.find((t) => t.id === depId);
 618 | 								if (depTask) {
 619 | 									// Add complexity to depTask before checking status
 620 | 									addComplexityToTask(depTask, complexityReport);
 621 | 									const isDone =
 622 | 										depTask.status === 'done' || depTask.status === 'completed';
 623 | 									const isInProgress = depTask.status === 'in-progress';
 624 | 									// Use the same color scheme as in formatDependenciesWithStatus
 625 | 									if (isDone) {
 626 | 										return chalk.green.bold(`${depId}`);
 627 | 									} else if (isInProgress) {
 628 | 										return chalk.hex('#FFA500').bold(`${depId}`);
 629 | 									} else {
 630 | 										return chalk.red.bold(`${depId}`);
 631 | 									}
 632 | 								}
 633 | 								return chalk.cyan(depId.toString());
 634 | 							})
 635 | 							.join(', ');
 636 | 
 637 | 						subtaskDepText = formattedDeps || chalk.gray('None');
 638 | 					}
 639 | 
 640 | 					// Add the subtask row without truncating dependencies
 641 | 					table.push([
 642 | 						`${task.id}.${subtask.id}`,
 643 | 						chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`),
 644 | 						getStatusWithColor(subtask.status, true),
 645 | 						chalk.dim('-'),
 646 | 						subtaskDepText,
 647 | 						subtask.complexityScore
 648 | 							? chalk.gray(`${subtask.complexityScore}`)
 649 | 							: chalk.gray('N/A')
 650 | 					]);
 651 | 				});
 652 | 			}
 653 | 		});
 654 | 
 655 | 		// Ensure we output the table even if it had to wrap
 656 | 		try {
 657 | 			console.log(table.toString());
 658 | 		} catch (err) {
 659 | 			log('error', `Error rendering table: ${err.message}`);
 660 | 
 661 | 			// Fall back to simpler output
 662 | 			console.log(
 663 | 				chalk.yellow(
 664 | 					'\nFalling back to simple task list due to terminal width constraints:'
 665 | 				)
 666 | 			);
 667 | 			filteredTasks.forEach((task) => {
 668 | 				console.log(
 669 | 					`${chalk.cyan(task.id)}: ${chalk.white(task.title)} - ${getStatusWithColor(task.status)}`
 670 | 				);
 671 | 			});
 672 | 		}
 673 | 
 674 | 		// Show filter info if applied
 675 | 		if (statusFilter) {
 676 | 			console.log(chalk.yellow(`\nFiltered by status: ${statusFilter}`));
 677 | 			console.log(
 678 | 				chalk.yellow(`Showing ${filteredTasks.length} of ${totalTasks} tasks`)
 679 | 			);
 680 | 		}
 681 | 
 682 | 		// Define priority colors
 683 | 		const priorityColors = {
 684 | 			high: chalk.red.bold,
 685 | 			medium: chalk.yellow,
 686 | 			low: chalk.gray
 687 | 		};
 688 | 
 689 | 		// Show next task box in a prominent color
 690 | 		if (nextItem) {
 691 | 			// Prepare subtasks section if they exist (Only tasks have .subtasks property)
 692 | 			let subtasksSection = '';
 693 | 			// Check if the nextItem is a top-level task before looking for subtasks
 694 | 			const parentTaskForSubtasks = data.tasks.find(
 695 | 				(t) => String(t.id) === String(nextItem.id)
 696 | 			); // Find the original task object
 697 | 			if (
 698 | 				parentTaskForSubtasks &&
 699 | 				parentTaskForSubtasks.subtasks &&
 700 | 				parentTaskForSubtasks.subtasks.length > 0
 701 | 			) {
 702 | 				subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`;
 703 | 				subtasksSection += parentTaskForSubtasks.subtasks
 704 | 					.map((subtask) => {
 705 | 						// Add complexity to subtask before display
 706 | 						addComplexityToTask(subtask, complexityReport);
 707 | 						// Using a more simplified format for subtask status display
 708 | 						const status = subtask.status || 'pending';
 709 | 						const statusColors = {
 710 | 							done: chalk.green,
 711 | 							completed: chalk.green,
 712 | 							pending: chalk.yellow,
 713 | 							'in-progress': chalk.blue,
 714 | 							deferred: chalk.gray,
 715 | 							blocked: chalk.red,
 716 | 							cancelled: chalk.gray
 717 | 						};
 718 | 						const statusColor =
 719 | 							statusColors[status.toLowerCase()] || chalk.white;
 720 | 						// Ensure subtask ID is displayed correctly using parent ID from the original task object
 721 | 						return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`;
 722 | 					})
 723 | 					.join('\n');
 724 | 			}
 725 | 
 726 | 			console.log(
 727 | 				boxen(
 728 | 					chalk.hex('#FF8800').bold(
 729 | 						// Use nextItem.id and nextItem.title
 730 | 						`🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}`
 731 | 					) +
 732 | 						'\n\n' +
 733 | 						// Use nextItem.priority, nextItem.status, nextItem.dependencies
 734 | 						`${chalk.white('Priority:')} ${priorityColors[nextItem.priority || 'medium'](nextItem.priority || 'medium')}   ${chalk.white('Status:')} ${getStatusWithColor(nextItem.status, true)}\n` +
 735 | 						`${chalk.white('Dependencies:')} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray('None')}\n\n` +
 736 | 						// Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this)
 737 | 						// *** Fetching original item for description and details ***
 738 | 						`${chalk.white('Description:')} ${getWorkItemDescription(nextItem, data.tasks)}` +
 739 | 						subtasksSection + // <-- Subtasks are handled above now
 740 | 						'\n\n' +
 741 | 						// Use nextItem.id
 742 | 						`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` +
 743 | 						// Use nextItem.id
 744 | 						`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${nextItem.id}`)}`,
 745 | 					{
 746 | 						padding: { left: 2, right: 2, top: 1, bottom: 1 },
 747 | 						borderColor: '#FF8800',
 748 | 						borderStyle: 'round',
 749 | 						margin: { top: 1, bottom: 1 },
 750 | 						title: '⚡ RECOMMENDED NEXT TASK ⚡',
 751 | 						titleAlignment: 'center',
 752 | 						width: terminalWidth - 4,
 753 | 						fullscreen: false
 754 | 					}
 755 | 				)
 756 | 			);
 757 | 		} else {
 758 | 			console.log(
 759 | 				boxen(
 760 | 					chalk.hex('#FF8800').bold('No eligible next task found') +
 761 | 						'\n\n' +
 762 | 						'All pending tasks have dependencies that are not yet completed, or all tasks are done.',
 763 | 					{
 764 | 						padding: 1,
 765 | 						borderColor: '#FF8800',
 766 | 						borderStyle: 'round',
 767 | 						margin: { top: 1, bottom: 1 },
 768 | 						title: '⚡ NEXT TASK ⚡',
 769 | 						titleAlignment: 'center',
 770 | 						width: terminalWidth - 4 // Use full terminal width minus a small margin
 771 | 					}
 772 | 				)
 773 | 			);
 774 | 		}
 775 | 
 776 | 		// Show next steps
 777 | 		console.log(
 778 | 			boxen(
 779 | 				chalk.white.bold('Suggested Next Steps:') +
 780 | 					'\n\n' +
 781 | 					`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next\n` +
 782 | 					`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` +
 783 | 					`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`,
 784 | 				{
 785 | 					padding: 1,
 786 | 					borderColor: 'gray',
 787 | 					borderStyle: 'round',
 788 | 					margin: { top: 1 }
 789 | 				}
 790 | 			)
 791 | 		);
 792 | 	} catch (error) {
 793 | 		log('error', `Error listing tasks: ${error.message}`);
 794 | 
 795 | 		if (outputFormat === 'json') {
 796 | 			// Return structured error for JSON output
 797 | 			throw {
 798 | 				code: 'TASK_LIST_ERROR',
 799 | 				message: error.message,
 800 | 				details: error.stack
 801 | 			};
 802 | 		}
 803 | 
 804 | 		console.error(chalk.red(`Error: ${error.message}`));
 805 | 		process.exit(1);
 806 | 	}
 807 | }
 808 | 
 809 | // *** Helper function to get description for task or subtask ***
 810 | function getWorkItemDescription(item, allTasks) {
 811 | 	if (!item) return 'N/A';
 812 | 	if (item.parentId) {
 813 | 		// It's a subtask
 814 | 		const parent = allTasks.find((t) => t.id === item.parentId);
 815 | 		const subtask = parent?.subtasks?.find(
 816 | 			(st) => `${parent.id}.${st.id}` === item.id
 817 | 		);
 818 | 		return subtask?.description || 'No description available.';
 819 | 	} else {
 820 | 		// It's a top-level task
 821 | 		const task = allTasks.find((t) => String(t.id) === String(item.id));
 822 | 		return task?.description || 'No description available.';
 823 | 	}
 824 | }
 825 | 
 826 | /**
 827 |  * Generate markdown-formatted output for README files
 828 |  * @param {Object} data - Full tasks data
 829 |  * @param {Array} filteredTasks - Filtered tasks array
 830 |  * @param {Object} stats - Statistics object
 831 |  * @returns {string} - Formatted markdown string
 832 |  */
 833 | function generateMarkdownOutput(data, filteredTasks, stats) {
 834 | 	const {
 835 | 		totalTasks,
 836 | 		completedTasks,
 837 | 		completionPercentage,
 838 | 		doneCount,
 839 | 		inProgressCount,
 840 | 		pendingCount,
 841 | 		blockedCount,
 842 | 		deferredCount,
 843 | 		cancelledCount,
 844 | 		totalSubtasks,
 845 | 		completedSubtasks,
 846 | 		subtaskCompletionPercentage,
 847 | 		inProgressSubtasks,
 848 | 		pendingSubtasks,
 849 | 		blockedSubtasks,
 850 | 		deferredSubtasks,
 851 | 		cancelledSubtasks,
 852 | 		tasksWithNoDeps,
 853 | 		tasksReadyToWork,
 854 | 		tasksWithUnsatisfiedDeps,
 855 | 		mostDependedOnTask,
 856 | 		mostDependedOnTaskId,
 857 | 		maxDependents,
 858 | 		avgDependenciesPerTask,
 859 | 		complexityReport,
 860 | 		withSubtasks,
 861 | 		nextItem
 862 | 	} = stats;
 863 | 
 864 | 	let markdown = '';
 865 | 
 866 | 	// Create progress bars for markdown (using Unicode block characters)
 867 | 	const createMarkdownProgressBar = (percentage, width = 20) => {
 868 | 		const filled = Math.round((percentage / 100) * width);
 869 | 		const empty = width - filled;
 870 | 		return '█'.repeat(filled) + '░'.repeat(empty);
 871 | 	};
 872 | 
 873 | 	const taskProgressBar = createMarkdownProgressBar(completionPercentage, 20);
 874 | 	const subtaskProgressBar = createMarkdownProgressBar(
 875 | 		subtaskCompletionPercentage,
 876 | 		20
 877 | 	);
 878 | 
 879 | 	// Dashboard section
 880 | 	// markdown += '```\n';
 881 | 	markdown += '| Project Dashboard |  |\n';
 882 | 	markdown += '| :-                |:-|\n';
 883 | 	markdown += `| Task Progress     | ${taskProgressBar} ${Math.round(completionPercentage)}% |\n`;
 884 | 	markdown += `| Done | ${doneCount} |\n`;
 885 | 	markdown += `| In Progress | ${inProgressCount} |\n`;
 886 | 	markdown += `| Pending | ${pendingCount} |\n`;
 887 | 	markdown += `| Deferred | ${deferredCount} |\n`;
 888 | 	markdown += `| Cancelled | ${cancelledCount} |\n`;
 889 | 	markdown += `|-|-|\n`;
 890 | 	markdown += `| Subtask Progress | ${subtaskProgressBar} ${Math.round(subtaskCompletionPercentage)}% |\n`;
 891 | 	markdown += `| Completed | ${completedSubtasks} |\n`;
 892 | 	markdown += `| In Progress | ${inProgressSubtasks} |\n`;
 893 | 	markdown += `| Pending | ${pendingSubtasks} |\n`;
 894 | 
 895 | 	markdown += '\n\n';
 896 | 
 897 | 	// Tasks table
 898 | 	markdown +=
 899 | 		'| ID | Title | Status | Priority | Dependencies | Complexity |\n';
 900 | 	markdown +=
 901 | 		'| :- | :-    | :-     | :-       | :-           | :-         |\n';
 902 | 
 903 | 	// Helper function to format status with symbols
 904 | 	const getStatusSymbol = (status) => {
 905 | 		switch (status) {
 906 | 			case 'done':
 907 | 			case 'completed':
 908 | 				return '✓&nbsp;done';
 909 | 			case 'in-progress':
 910 | 				return '►&nbsp;in-progress';
 911 | 			case 'pending':
 912 | 				return '○&nbsp;pending';
 913 | 			case 'blocked':
 914 | 				return '⭕&nbsp;blocked';
 915 | 			case 'deferred':
 916 | 				return 'x&nbsp;deferred';
 917 | 			case 'cancelled':
 918 | 				return 'x&nbsp;cancelled';
 919 | 			case 'review':
 920 | 				return '?&nbsp;review';
 921 | 			default:
 922 | 				return status || 'pending';
 923 | 		}
 924 | 	};
 925 | 
 926 | 	// Helper function to format dependencies without color codes
 927 | 	const formatDependenciesForMarkdown = (deps, allTasks) => {
 928 | 		if (!deps || deps.length === 0) return 'None';
 929 | 		return deps
 930 | 			.map((depId) => {
 931 | 				const depTask = allTasks.find((t) => t.id === depId);
 932 | 				return depTask ? depId.toString() : depId.toString();
 933 | 			})
 934 | 			.join(', ');
 935 | 	};
 936 | 
 937 | 	// Process all tasks
 938 | 	filteredTasks.forEach((task) => {
 939 | 		const taskTitle = task.title; // No truncation for README
 940 | 		const statusSymbol = getStatusSymbol(task.status);
 941 | 		const priority = task.priority || 'medium';
 942 | 		const deps = formatDependenciesForMarkdown(task.dependencies, data.tasks);
 943 | 		const complexity = task.complexityScore
 944 | 			? `● ${task.complexityScore}`
 945 | 			: 'N/A';
 946 | 
 947 | 		markdown += `| ${task.id} | ${taskTitle} | ${statusSymbol} | ${priority} | ${deps} | ${complexity} |\n`;
 948 | 
 949 | 		// Add subtasks if requested
 950 | 		if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
 951 | 			task.subtasks.forEach((subtask) => {
 952 | 				const subtaskTitle = `${subtask.title}`; // No truncation
 953 | 				const subtaskStatus = getStatusSymbol(subtask.status);
 954 | 				const subtaskDeps = formatDependenciesForMarkdown(
 955 | 					subtask.dependencies,
 956 | 					data.tasks
 957 | 				);
 958 | 				const subtaskComplexity = subtask.complexityScore
 959 | 					? subtask.complexityScore.toString()
 960 | 					: 'N/A';
 961 | 
 962 | 				markdown += `| ${task.id}.${subtask.id} | ${subtaskTitle} | ${subtaskStatus} | -            | ${subtaskDeps} | ${subtaskComplexity} |\n`;
 963 | 			});
 964 | 		}
 965 | 	});
 966 | 
 967 | 	return markdown;
 968 | }
 969 | 
 970 | /**
 971 |  * Format dependencies for compact output with truncation and coloring
 972 |  * @param {Array} dependencies - Array of dependency IDs
 973 |  * @returns {string} - Formatted dependency string with arrow prefix
 974 |  */
 975 | function formatCompactDependencies(dependencies) {
 976 | 	if (!dependencies || dependencies.length === 0) {
 977 | 		return '';
 978 | 	}
 979 | 
 980 | 	if (dependencies.length > 5) {
 981 | 		const visible = dependencies.slice(0, 5).join(',');
 982 | 		const remaining = dependencies.length - 5;
 983 | 		return ` → ${chalk.cyan(visible)}${chalk.gray('... (+' + remaining + ' more)')}`;
 984 | 	} else {
 985 | 		return ` → ${chalk.cyan(dependencies.join(','))}`;
 986 | 	}
 987 | }
 988 | 
 989 | /**
 990 |  * Format a single task in compact one-line format
 991 |  * @param {Object} task - Task object
 992 |  * @param {number} maxTitleLength - Maximum title length before truncation
 993 |  * @returns {string} - Formatted task line
 994 |  */
 995 | function formatCompactTask(task, maxTitleLength = 50) {
 996 | 	const status = task.status || 'pending';
 997 | 	const priority = task.priority || 'medium';
 998 | 	const title = truncate(task.title || 'Untitled', maxTitleLength);
 999 | 
1000 | 	// Use colored status from existing function
1001 | 	const coloredStatus = getStatusWithColor(status, true);
1002 | 
1003 | 	// Color priority based on level
1004 | 	const priorityColors = {
1005 | 		high: chalk.red,
1006 | 		medium: chalk.yellow,
1007 | 		low: chalk.gray
1008 | 	};
1009 | 	const priorityColor = priorityColors[priority] || chalk.white;
1010 | 
1011 | 	// Format dependencies using shared helper
1012 | 	const depsText = formatCompactDependencies(task.dependencies);
1013 | 
1014 | 	return `${chalk.cyan(task.id)} ${coloredStatus} ${chalk.white(title)} ${priorityColor('(' + priority + ')')}${depsText}`;
1015 | }
1016 | 
1017 | /**
1018 |  * Format a subtask in compact format with indentation
1019 |  * @param {Object} subtask - Subtask object
1020 |  * @param {string|number} parentId - Parent task ID
1021 |  * @param {number} maxTitleLength - Maximum title length before truncation
1022 |  * @returns {string} - Formatted subtask line
1023 |  */
1024 | function formatCompactSubtask(subtask, parentId, maxTitleLength = 47) {
1025 | 	const status = subtask.status || 'pending';
1026 | 	const title = truncate(subtask.title || 'Untitled', maxTitleLength);
1027 | 
1028 | 	// Use colored status from existing function
1029 | 	const coloredStatus = getStatusWithColor(status, true);
1030 | 
1031 | 	// Format dependencies using shared helper
1032 | 	const depsText = formatCompactDependencies(subtask.dependencies);
1033 | 
1034 | 	return `  ${chalk.cyan(parentId + '.' + subtask.id)} ${coloredStatus} ${chalk.dim(title)}${depsText}`;
1035 | }
1036 | 
1037 | /**
1038 |  * Render complete compact output
1039 |  * @param {Array} filteredTasks - Tasks to display
1040 |  * @param {boolean} withSubtasks - Whether to include subtasks
1041 |  * @returns {void} - Outputs directly to console
1042 |  */
1043 | function renderCompactOutput(filteredTasks, withSubtasks) {
1044 | 	if (filteredTasks.length === 0) {
1045 | 		console.log('No tasks found');
1046 | 		return;
1047 | 	}
1048 | 
1049 | 	const output = [];
1050 | 
1051 | 	filteredTasks.forEach((task) => {
1052 | 		output.push(formatCompactTask(task));
1053 | 
1054 | 		if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
1055 | 			task.subtasks.forEach((subtask) => {
1056 | 				output.push(formatCompactSubtask(subtask, task.id));
1057 | 			});
1058 | 		}
1059 | 	});
1060 | 
1061 | 	console.log(output.join('\n'));
1062 | }
1063 | 
1064 | export default listTasks;
1065 | 
```
Page 41/52FirstPrevNextLast