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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/git/adapters/git-adapter.test.ts:
--------------------------------------------------------------------------------

```typescript
   1 | import os from 'os';
   2 | import path from 'path';
   3 | import {
   4 | 	afterEach,
   5 | 	beforeEach,
   6 | 	describe,
   7 | 	expect,
   8 | 	it,
   9 | 	jest
  10 | } from '@jest/globals';
  11 | import fs from 'fs-extra';
  12 | import { GitAdapter } from '../../../../../packages/tm-core/src/git/git-adapter.js';
  13 | 
  14 | describe('GitAdapter - Repository Detection and Validation', () => {
  15 | 	let testDir;
  16 | 	let gitAdapter;
  17 | 
  18 | 	beforeEach(async () => {
  19 | 		// Create temporary test directory
  20 | 		testDir = path.join(os.tmpdir(), `git-test-${Date.now()}`);
  21 | 		await fs.ensureDir(testDir);
  22 | 	});
  23 | 
  24 | 	afterEach(async () => {
  25 | 		// Clean up test directory
  26 | 		await fs.remove(testDir);
  27 | 	});
  28 | 
  29 | 	describe('isGitRepository', () => {
  30 | 		it('should return false for non-git directory', async () => {
  31 | 			gitAdapter = new GitAdapter(testDir);
  32 | 
  33 | 			const isRepo = await gitAdapter.isGitRepository();
  34 | 
  35 | 			expect(isRepo).toBe(false);
  36 | 		});
  37 | 
  38 | 		it('should return true for git repository', async () => {
  39 | 			// Initialize real git repo
  40 | 			await fs.ensureDir(path.join(testDir, '.git'));
  41 | 			await fs.ensureDir(path.join(testDir, '.git', 'objects'));
  42 | 			await fs.ensureDir(path.join(testDir, '.git', 'refs'));
  43 | 			await fs.writeFile(
  44 | 				path.join(testDir, '.git', 'HEAD'),
  45 | 				'ref: refs/heads/main\n'
  46 | 			);
  47 | 
  48 | 			gitAdapter = new GitAdapter(testDir);
  49 | 
  50 | 			const isRepo = await gitAdapter.isGitRepository();
  51 | 
  52 | 			expect(isRepo).toBe(true);
  53 | 		});
  54 | 
  55 | 		it('should detect git repository in subdirectory', async () => {
  56 | 			// Initialize real git repo in parent
  57 | 			await fs.ensureDir(path.join(testDir, '.git'));
  58 | 			await fs.ensureDir(path.join(testDir, '.git', 'objects'));
  59 | 			await fs.ensureDir(path.join(testDir, '.git', 'refs'));
  60 | 			await fs.writeFile(
  61 | 				path.join(testDir, '.git', 'HEAD'),
  62 | 				'ref: refs/heads/main\n'
  63 | 			);
  64 | 
  65 | 			// Create subdirectory
  66 | 			const subDir = path.join(testDir, 'src', 'components');
  67 | 			await fs.ensureDir(subDir);
  68 | 
  69 | 			gitAdapter = new GitAdapter(subDir);
  70 | 
  71 | 			const isRepo = await gitAdapter.isGitRepository();
  72 | 
  73 | 			expect(isRepo).toBe(true);
  74 | 		});
  75 | 
  76 | 		it('should handle directory with .git file (submodule)', async () => {
  77 | 			// Create .git file (used in submodules/worktrees)
  78 | 			await fs.writeFile(path.join(testDir, '.git'), 'gitdir: /path/to/git');
  79 | 
  80 | 			gitAdapter = new GitAdapter(testDir);
  81 | 
  82 | 			const isRepo = await gitAdapter.isGitRepository();
  83 | 
  84 | 			expect(isRepo).toBe(true);
  85 | 		});
  86 | 
  87 | 		it('should return false if .git is neither file nor directory', async () => {
  88 | 			gitAdapter = new GitAdapter(testDir);
  89 | 
  90 | 			const isRepo = await gitAdapter.isGitRepository();
  91 | 
  92 | 			expect(isRepo).toBe(false);
  93 | 		});
  94 | 	});
  95 | 
  96 | 	describe('validateGitInstallation', () => {
  97 | 		it('should validate git is installed', async () => {
  98 | 			gitAdapter = new GitAdapter(testDir);
  99 | 
 100 | 			await expect(gitAdapter.validateGitInstallation()).resolves.not.toThrow();
 101 | 		});
 102 | 
 103 | 		it('should throw error if git version check fails', async () => {
 104 | 			gitAdapter = new GitAdapter(testDir);
 105 | 
 106 | 			// Mock simple-git to throw error
 107 | 			const mockGit = {
 108 | 				version: jest.fn().mockRejectedValue(new Error('git not found'))
 109 | 			};
 110 | 			gitAdapter.git = mockGit;
 111 | 
 112 | 			await expect(gitAdapter.validateGitInstallation()).rejects.toThrow(
 113 | 				'git not found'
 114 | 			);
 115 | 		});
 116 | 
 117 | 		it('should return git version info', async () => {
 118 | 			gitAdapter = new GitAdapter(testDir);
 119 | 
 120 | 			const versionInfo = await gitAdapter.getGitVersion();
 121 | 
 122 | 			expect(versionInfo).toBeDefined();
 123 | 			expect(versionInfo.major).toBeGreaterThan(0);
 124 | 		});
 125 | 	});
 126 | 
 127 | 	describe('getRepositoryRoot', () => {
 128 | 		it('should return repository root path', async () => {
 129 | 			// Initialize real git repo
 130 | 			await fs.ensureDir(path.join(testDir, '.git'));
 131 | 			await fs.ensureDir(path.join(testDir, '.git', 'objects'));
 132 | 			await fs.ensureDir(path.join(testDir, '.git', 'refs'));
 133 | 			await fs.writeFile(
 134 | 				path.join(testDir, '.git', 'HEAD'),
 135 | 				'ref: refs/heads/main\n'
 136 | 			);
 137 | 
 138 | 			gitAdapter = new GitAdapter(testDir);
 139 | 
 140 | 			const root = await gitAdapter.getRepositoryRoot();
 141 | 
 142 | 			// Resolve both paths to handle symlinks (e.g., /var vs /private/var on macOS)
 143 | 			expect(await fs.realpath(root)).toBe(await fs.realpath(testDir));
 144 | 		});
 145 | 
 146 | 		it('should find repository root from subdirectory', async () => {
 147 | 			// Initialize real git repo in parent
 148 | 			await fs.ensureDir(path.join(testDir, '.git'));
 149 | 			await fs.ensureDir(path.join(testDir, '.git', 'objects'));
 150 | 			await fs.ensureDir(path.join(testDir, '.git', 'refs'));
 151 | 			await fs.writeFile(
 152 | 				path.join(testDir, '.git', 'HEAD'),
 153 | 				'ref: refs/heads/main\n'
 154 | 			);
 155 | 
 156 | 			// Create subdirectory
 157 | 			const subDir = path.join(testDir, 'src', 'components');
 158 | 			await fs.ensureDir(subDir);
 159 | 
 160 | 			gitAdapter = new GitAdapter(subDir);
 161 | 
 162 | 			const root = await gitAdapter.getRepositoryRoot();
 163 | 
 164 | 			// Resolve both paths to handle symlinks (e.g., /var vs /private/var on macOS)
 165 | 			expect(await fs.realpath(root)).toBe(await fs.realpath(testDir));
 166 | 		});
 167 | 
 168 | 		it('should throw error if not in git repository', async () => {
 169 | 			gitAdapter = new GitAdapter(testDir);
 170 | 
 171 | 			await expect(gitAdapter.getRepositoryRoot()).rejects.toThrow(
 172 | 				'not a git repository'
 173 | 			);
 174 | 		});
 175 | 	});
 176 | 
 177 | 	describe('validateRepository', () => {
 178 | 		it('should validate repository is in good state', async () => {
 179 | 			// Initialize git repo
 180 | 			await fs.ensureDir(path.join(testDir, '.git'));
 181 | 			await fs.ensureDir(path.join(testDir, '.git', 'refs'));
 182 | 			await fs.ensureDir(path.join(testDir, '.git', 'objects'));
 183 | 			await fs.writeFile(
 184 | 				path.join(testDir, '.git', 'HEAD'),
 185 | 				'ref: refs/heads/main\n'
 186 | 			);
 187 | 
 188 | 			gitAdapter = new GitAdapter(testDir);
 189 | 
 190 | 			await expect(gitAdapter.validateRepository()).resolves.not.toThrow();
 191 | 		});
 192 | 
 193 | 		it('should throw error for non-git directory', async () => {
 194 | 			gitAdapter = new GitAdapter(testDir);
 195 | 
 196 | 			await expect(gitAdapter.validateRepository()).rejects.toThrow(
 197 | 				'not a git repository'
 198 | 			);
 199 | 		});
 200 | 
 201 | 		it('should detect corrupted repository', async () => {
 202 | 			// Create .git directory but make it empty (corrupted)
 203 | 			await fs.ensureDir(path.join(testDir, '.git'));
 204 | 
 205 | 			gitAdapter = new GitAdapter(testDir);
 206 | 
 207 | 			// This should either succeed or throw a specific error
 208 | 			// depending on simple-git's behavior
 209 | 			try {
 210 | 				await gitAdapter.validateRepository();
 211 | 			} catch (error) {
 212 | 				expect(error.message).toMatch(/repository|git/i);
 213 | 			}
 214 | 		});
 215 | 	});
 216 | 
 217 | 	describe('ensureGitRepository', () => {
 218 | 		it('should not throw if in valid git repository', async () => {
 219 | 			// Initialize git repo
 220 | 			await fs.ensureDir(path.join(testDir, '.git'));
 221 | 
 222 | 			gitAdapter = new GitAdapter(testDir);
 223 | 
 224 | 			await expect(gitAdapter.ensureGitRepository()).resolves.not.toThrow();
 225 | 		});
 226 | 
 227 | 		it('should throw error if not in git repository', async () => {
 228 | 			gitAdapter = new GitAdapter(testDir);
 229 | 
 230 | 			await expect(gitAdapter.ensureGitRepository()).rejects.toThrow(
 231 | 				'not a git repository'
 232 | 			);
 233 | 		});
 234 | 
 235 | 		it('should provide helpful error message', async () => {
 236 | 			gitAdapter = new GitAdapter(testDir);
 237 | 
 238 | 			try {
 239 | 				await gitAdapter.ensureGitRepository();
 240 | 				fail('Should have thrown error');
 241 | 			} catch (error) {
 242 | 				expect(error.message).toContain('not a git repository');
 243 | 				expect(error.message).toContain(testDir);
 244 | 			}
 245 | 		});
 246 | 	});
 247 | 
 248 | 	describe('constructor', () => {
 249 | 		it('should create GitAdapter with project path', () => {
 250 | 			gitAdapter = new GitAdapter(testDir);
 251 | 
 252 | 			expect(gitAdapter).toBeDefined();
 253 | 			expect(gitAdapter.projectPath).toBe(testDir);
 254 | 		});
 255 | 
 256 | 		it('should normalize project path', () => {
 257 | 			const unnormalizedPath = path.join(testDir, '..', path.basename(testDir));
 258 | 			gitAdapter = new GitAdapter(unnormalizedPath);
 259 | 
 260 | 			expect(gitAdapter.projectPath).toBe(testDir);
 261 | 		});
 262 | 
 263 | 		it('should initialize simple-git instance', () => {
 264 | 			gitAdapter = new GitAdapter(testDir);
 265 | 
 266 | 			expect(gitAdapter.git).toBeDefined();
 267 | 		});
 268 | 
 269 | 		it('should throw error for invalid path', () => {
 270 | 			expect(() => new GitAdapter('')).toThrow('Project path is required');
 271 | 		});
 272 | 
 273 | 		it('should throw error for non-absolute path', () => {
 274 | 			expect(() => new GitAdapter('./relative/path')).toThrow('absolute');
 275 | 		});
 276 | 	});
 277 | 
 278 | 	describe('error handling', () => {
 279 | 		it('should provide clear error for permission denied', async () => {
 280 | 			// Create .git but make it inaccessible
 281 | 			await fs.ensureDir(path.join(testDir, '.git'));
 282 | 
 283 | 			gitAdapter = new GitAdapter(testDir);
 284 | 
 285 | 			try {
 286 | 				await fs.chmod(path.join(testDir, '.git'), 0o000);
 287 | 
 288 | 				await gitAdapter.isGitRepository();
 289 | 			} catch (error) {
 290 | 				// Error handling
 291 | 			} finally {
 292 | 				// Restore permissions
 293 | 				await fs.chmod(path.join(testDir, '.git'), 0o755);
 294 | 			}
 295 | 		});
 296 | 
 297 | 		it('should handle symbolic links correctly', async () => {
 298 | 			// Create actual git repo
 299 | 			const realRepo = path.join(testDir, 'real-repo');
 300 | 			await fs.ensureDir(path.join(realRepo, '.git'));
 301 | 
 302 | 			// Create symlink
 303 | 			const symlinkPath = path.join(testDir, 'symlink-repo');
 304 | 			try {
 305 | 				await fs.symlink(realRepo, symlinkPath);
 306 | 
 307 | 				gitAdapter = new GitAdapter(symlinkPath);
 308 | 
 309 | 				const isRepo = await gitAdapter.isGitRepository();
 310 | 
 311 | 				expect(isRepo).toBe(true);
 312 | 			} catch (error) {
 313 | 				// Skip test on platforms without symlink support
 314 | 				if (error.code !== 'EPERM') {
 315 | 					throw error;
 316 | 				}
 317 | 			}
 318 | 		});
 319 | 	});
 320 | 
 321 | 	describe('integration with simple-git', () => {
 322 | 		it('should use simple-git for git operations', () => {
 323 | 			gitAdapter = new GitAdapter(testDir);
 324 | 
 325 | 			// Check that git instance is from simple-git
 326 | 			expect(typeof gitAdapter.git.status).toBe('function');
 327 | 			expect(typeof gitAdapter.git.branch).toBe('function');
 328 | 		});
 329 | 
 330 | 		it('should pass correct working directory to simple-git', () => {
 331 | 			gitAdapter = new GitAdapter(testDir);
 332 | 
 333 | 			// simple-git should be initialized with testDir
 334 | 			expect(gitAdapter.git._executor).toBeDefined();
 335 | 		});
 336 | 	});
 337 | });
 338 | 
 339 | describe('GitAdapter - Working Tree Status', () => {
 340 | 	let testDir;
 341 | 	let gitAdapter;
 342 | 	let simpleGit;
 343 | 
 344 | 	beforeEach(async () => {
 345 | 		testDir = path.join(os.tmpdir(), `git-status-test-${Date.now()}`);
 346 | 		await fs.ensureDir(testDir);
 347 | 
 348 | 		// Initialize actual git repo
 349 | 		simpleGit = (await import('simple-git')).default;
 350 | 		const git = simpleGit(testDir);
 351 | 		await git.init();
 352 | 		await git.addConfig('user.name', 'Test User');
 353 | 		await git.addConfig('user.email', '[email protected]');
 354 | 
 355 | 		gitAdapter = new GitAdapter(testDir);
 356 | 	});
 357 | 
 358 | 	afterEach(async () => {
 359 | 		await fs.remove(testDir);
 360 | 	});
 361 | 
 362 | 	describe('isWorkingTreeClean', () => {
 363 | 		it('should return true for clean working tree', async () => {
 364 | 			const isClean = await gitAdapter.isWorkingTreeClean();
 365 | 			expect(isClean).toBe(true);
 366 | 		});
 367 | 
 368 | 		it('should return false when files are modified', async () => {
 369 | 			// Create and commit a file
 370 | 			await fs.writeFile(path.join(testDir, 'test.txt'), 'initial');
 371 | 			const git = simpleGit(testDir);
 372 | 			await git.add('test.txt');
 373 | 			await git.commit('initial commit', undefined, { '--no-gpg-sign': null });
 374 | 
 375 | 			// Modify the file
 376 | 			await fs.writeFile(path.join(testDir, 'test.txt'), 'modified');
 377 | 
 378 | 			const isClean = await gitAdapter.isWorkingTreeClean();
 379 | 			expect(isClean).toBe(false);
 380 | 		});
 381 | 
 382 | 		it('should return false when untracked files exist', async () => {
 383 | 			await fs.writeFile(path.join(testDir, 'untracked.txt'), 'content');
 384 | 
 385 | 			const isClean = await gitAdapter.isWorkingTreeClean();
 386 | 			expect(isClean).toBe(false);
 387 | 		});
 388 | 
 389 | 		it('should return false when files are staged', async () => {
 390 | 			await fs.writeFile(path.join(testDir, 'staged.txt'), 'content');
 391 | 			const git = simpleGit(testDir);
 392 | 			await git.add('staged.txt');
 393 | 
 394 | 			const isClean = await gitAdapter.isWorkingTreeClean();
 395 | 			expect(isClean).toBe(false);
 396 | 		});
 397 | 	});
 398 | 
 399 | 	describe('getStatus', () => {
 400 | 		it('should return status for clean repo', async () => {
 401 | 			const status = await gitAdapter.getStatus();
 402 | 
 403 | 			expect(status).toBeDefined();
 404 | 			expect(status.modified).toEqual([]);
 405 | 			expect(status.not_added).toEqual([]);
 406 | 			expect(status.deleted).toEqual([]);
 407 | 			expect(status.created).toEqual([]);
 408 | 		});
 409 | 
 410 | 		it('should detect modified files', async () => {
 411 | 			// Create and commit
 412 | 			await fs.writeFile(path.join(testDir, 'test.txt'), 'initial');
 413 | 			const git = simpleGit(testDir);
 414 | 			await git.add('test.txt');
 415 | 			await git.commit('initial', undefined, { '--no-gpg-sign': null });
 416 | 
 417 | 			// Modify
 418 | 			await fs.writeFile(path.join(testDir, 'test.txt'), 'modified');
 419 | 
 420 | 			const status = await gitAdapter.getStatus();
 421 | 			expect(status.modified).toContain('test.txt');
 422 | 		});
 423 | 
 424 | 		it('should detect untracked files', async () => {
 425 | 			await fs.writeFile(path.join(testDir, 'untracked.txt'), 'content');
 426 | 
 427 | 			const status = await gitAdapter.getStatus();
 428 | 			expect(status.not_added).toContain('untracked.txt');
 429 | 		});
 430 | 
 431 | 		it('should detect staged files', async () => {
 432 | 			await fs.writeFile(path.join(testDir, 'staged.txt'), 'content');
 433 | 			const git = simpleGit(testDir);
 434 | 			await git.add('staged.txt');
 435 | 
 436 | 			const status = await gitAdapter.getStatus();
 437 | 			expect(status.created).toContain('staged.txt');
 438 | 		});
 439 | 
 440 | 		it('should detect deleted files', async () => {
 441 | 			// Create and commit
 442 | 			await fs.writeFile(path.join(testDir, 'deleted.txt'), 'content');
 443 | 			const git = simpleGit(testDir);
 444 | 			await git.add('deleted.txt');
 445 | 			await git.commit('add file', undefined, { '--no-gpg-sign': null });
 446 | 
 447 | 			// Delete
 448 | 			await fs.remove(path.join(testDir, 'deleted.txt'));
 449 | 
 450 | 			const status = await gitAdapter.getStatus();
 451 | 			expect(status.deleted).toContain('deleted.txt');
 452 | 		});
 453 | 	});
 454 | 
 455 | 	describe('hasUncommittedChanges', () => {
 456 | 		it('should return false for clean repo', async () => {
 457 | 			const hasChanges = await gitAdapter.hasUncommittedChanges();
 458 | 			expect(hasChanges).toBe(false);
 459 | 		});
 460 | 
 461 | 		it('should return true for modified files', async () => {
 462 | 			await fs.writeFile(path.join(testDir, 'test.txt'), 'initial');
 463 | 			const git = simpleGit(testDir);
 464 | 			await git.add('test.txt');
 465 | 			await git.commit('initial', undefined, { '--no-gpg-sign': null });
 466 | 
 467 | 			await fs.writeFile(path.join(testDir, 'test.txt'), 'modified');
 468 | 
 469 | 			const hasChanges = await gitAdapter.hasUncommittedChanges();
 470 | 			expect(hasChanges).toBe(true);
 471 | 		});
 472 | 
 473 | 		it('should return true for staged changes', async () => {
 474 | 			await fs.writeFile(path.join(testDir, 'staged.txt'), 'content');
 475 | 			const git = simpleGit(testDir);
 476 | 			await git.add('staged.txt');
 477 | 
 478 | 			const hasChanges = await gitAdapter.hasUncommittedChanges();
 479 | 			expect(hasChanges).toBe(true);
 480 | 		});
 481 | 	});
 482 | 
 483 | 	describe('hasStagedChanges', () => {
 484 | 		it('should return false when no staged changes', async () => {
 485 | 			const hasStaged = await gitAdapter.hasStagedChanges();
 486 | 			expect(hasStaged).toBe(false);
 487 | 		});
 488 | 
 489 | 		it('should return true when files are staged', async () => {
 490 | 			await fs.writeFile(path.join(testDir, 'staged.txt'), 'content');
 491 | 			const git = simpleGit(testDir);
 492 | 			await git.add('staged.txt');
 493 | 
 494 | 			const hasStaged = await gitAdapter.hasStagedChanges();
 495 | 			expect(hasStaged).toBe(true);
 496 | 		});
 497 | 
 498 | 		it('should return false for unstaged changes only', async () => {
 499 | 			await fs.writeFile(path.join(testDir, 'test.txt'), 'initial');
 500 | 			const git = simpleGit(testDir);
 501 | 			await git.add('test.txt');
 502 | 			await git.commit('initial', undefined, { '--no-gpg-sign': null });
 503 | 
 504 | 			await fs.writeFile(path.join(testDir, 'test.txt'), 'modified');
 505 | 
 506 | 			const hasStaged = await gitAdapter.hasStagedChanges();
 507 | 			expect(hasStaged).toBe(false);
 508 | 		});
 509 | 	});
 510 | 
 511 | 	describe('hasUntrackedFiles', () => {
 512 | 		it('should return false when no untracked files', async () => {
 513 | 			const hasUntracked = await gitAdapter.hasUntrackedFiles();
 514 | 			expect(hasUntracked).toBe(false);
 515 | 		});
 516 | 
 517 | 		it('should return true when untracked files exist', async () => {
 518 | 			await fs.writeFile(path.join(testDir, 'untracked.txt'), 'content');
 519 | 
 520 | 			const hasUntracked = await gitAdapter.hasUntrackedFiles();
 521 | 			expect(hasUntracked).toBe(true);
 522 | 		});
 523 | 
 524 | 		it('should not count staged files as untracked', async () => {
 525 | 			await fs.writeFile(path.join(testDir, 'staged.txt'), 'content');
 526 | 			const git = simpleGit(testDir);
 527 | 			await git.add('staged.txt');
 528 | 
 529 | 			const hasUntracked = await gitAdapter.hasUntrackedFiles();
 530 | 			expect(hasUntracked).toBe(false);
 531 | 		});
 532 | 	});
 533 | 
 534 | 	describe('getStatusSummary', () => {
 535 | 		it('should provide summary for clean repo', async () => {
 536 | 			const summary = await gitAdapter.getStatusSummary();
 537 | 
 538 | 			expect(summary).toBeDefined();
 539 | 			expect(summary.isClean).toBe(true);
 540 | 			expect(summary.totalChanges).toBe(0);
 541 | 		});
 542 | 
 543 | 		it('should count all types of changes', async () => {
 544 | 			// Create committed file
 545 | 			await fs.writeFile(path.join(testDir, 'committed.txt'), 'content');
 546 | 			const git = simpleGit(testDir);
 547 | 			await git.add('committed.txt');
 548 | 			await git.commit('initial', undefined, { '--no-gpg-sign': null });
 549 | 
 550 | 			// Modify it
 551 | 			await fs.writeFile(path.join(testDir, 'committed.txt'), 'modified');
 552 | 
 553 | 			// Add untracked
 554 | 			await fs.writeFile(path.join(testDir, 'untracked.txt'), 'content');
 555 | 
 556 | 			// Add staged
 557 | 			await fs.writeFile(path.join(testDir, 'staged.txt'), 'content');
 558 | 			await git.add('staged.txt');
 559 | 
 560 | 			const summary = await gitAdapter.getStatusSummary();
 561 | 
 562 | 			expect(summary.isClean).toBe(false);
 563 | 			expect(summary.totalChanges).toBeGreaterThan(0);
 564 | 			expect(summary.modified).toBeGreaterThan(0);
 565 | 			expect(summary.untracked).toBeGreaterThan(0);
 566 | 			expect(summary.staged).toBeGreaterThan(0);
 567 | 		});
 568 | 	});
 569 | 
 570 | 	describe('ensureCleanWorkingTree', () => {
 571 | 		it('should not throw for clean repo', async () => {
 572 | 			await expect(gitAdapter.ensureCleanWorkingTree()).resolves.not.toThrow();
 573 | 		});
 574 | 
 575 | 		it('should throw for dirty repo', async () => {
 576 | 			await fs.writeFile(path.join(testDir, 'dirty.txt'), 'content');
 577 | 
 578 | 			await expect(gitAdapter.ensureCleanWorkingTree()).rejects.toThrow(
 579 | 				'working tree is not clean'
 580 | 			);
 581 | 		});
 582 | 
 583 | 		it('should provide details about changes in error', async () => {
 584 | 			await fs.writeFile(path.join(testDir, 'modified.txt'), 'content');
 585 | 
 586 | 			try {
 587 | 				await gitAdapter.ensureCleanWorkingTree();
 588 | 				fail('Should have thrown');
 589 | 			} catch (error) {
 590 | 				expect(error.message).toContain('working tree is not clean');
 591 | 			}
 592 | 		});
 593 | 	});
 594 | 
 595 | 	describe('GitAdapter - Branch Operations', () => {
 596 | 		let testDir;
 597 | 		let gitAdapter;
 598 | 		let simpleGit;
 599 | 
 600 | 		beforeEach(async () => {
 601 | 			testDir = path.join(os.tmpdir(), `git-branch-test-${Date.now()}`);
 602 | 			await fs.ensureDir(testDir);
 603 | 
 604 | 			// Initialize actual git repo with initial commit
 605 | 			simpleGit = (await import('simple-git')).default;
 606 | 			const git = simpleGit(testDir);
 607 | 			await git.init();
 608 | 			await git.addConfig('user.name', 'Test User');
 609 | 			await git.addConfig('user.email', '[email protected]');
 610 | 
 611 | 			// Create initial commit
 612 | 			await fs.writeFile(path.join(testDir, 'README.md'), '# Test Repo');
 613 | 			await git.add('README.md');
 614 | 			await git.commit('Initial commit', undefined, { '--no-gpg-sign': null });
 615 | 
 616 | 			// Rename master to main for consistency
 617 | 			try {
 618 | 				await git.branch(['-m', 'master', 'main']);
 619 | 			} catch (error) {
 620 | 				// Branch might already be main, ignore error
 621 | 			}
 622 | 
 623 | 			gitAdapter = new GitAdapter(testDir);
 624 | 		});
 625 | 
 626 | 		afterEach(async () => {
 627 | 			if (await fs.pathExists(testDir)) {
 628 | 				await fs.remove(testDir);
 629 | 			}
 630 | 		});
 631 | 
 632 | 		describe('getCurrentBranch', () => {
 633 | 			it('should return current branch name', async () => {
 634 | 				const branch = await gitAdapter.getCurrentBranch();
 635 | 				expect(branch).toBe('main');
 636 | 			});
 637 | 
 638 | 			it('should return updated branch after checkout', async () => {
 639 | 				const git = simpleGit(testDir);
 640 | 				await git.checkoutLocalBranch('feature');
 641 | 
 642 | 				const branch = await gitAdapter.getCurrentBranch();
 643 | 				expect(branch).toBe('feature');
 644 | 			});
 645 | 		});
 646 | 
 647 | 		describe('listBranches', () => {
 648 | 			it('should list all branches', async () => {
 649 | 				const git = simpleGit(testDir);
 650 | 				await git.checkoutLocalBranch('feature-a');
 651 | 				await git.checkout('main');
 652 | 				await git.checkoutLocalBranch('feature-b');
 653 | 
 654 | 				const branches = await gitAdapter.listBranches();
 655 | 				expect(branches).toContain('main');
 656 | 				expect(branches).toContain('feature-a');
 657 | 				expect(branches).toContain('feature-b');
 658 | 				expect(branches.length).toBeGreaterThanOrEqual(3);
 659 | 			});
 660 | 
 661 | 			it('should return empty array if only on detached HEAD', async () => {
 662 | 				const git = simpleGit(testDir);
 663 | 				const log = await git.log();
 664 | 				await git.checkout(log.latest.hash);
 665 | 
 666 | 				const branches = await gitAdapter.listBranches();
 667 | 				expect(Array.isArray(branches)).toBe(true);
 668 | 			});
 669 | 		});
 670 | 
 671 | 		describe('branchExists', () => {
 672 | 			it('should return true for existing branch', async () => {
 673 | 				const exists = await gitAdapter.branchExists('main');
 674 | 				expect(exists).toBe(true);
 675 | 			});
 676 | 
 677 | 			it('should return false for non-existing branch', async () => {
 678 | 				const exists = await gitAdapter.branchExists('nonexistent');
 679 | 				expect(exists).toBe(false);
 680 | 			});
 681 | 
 682 | 			it('should detect newly created branches', async () => {
 683 | 				const git = simpleGit(testDir);
 684 | 				await git.checkoutLocalBranch('new-feature');
 685 | 
 686 | 				const exists = await gitAdapter.branchExists('new-feature');
 687 | 				expect(exists).toBe(true);
 688 | 			});
 689 | 		});
 690 | 
 691 | 		describe('createBranch', () => {
 692 | 			it('should create a new branch', async () => {
 693 | 				await gitAdapter.createBranch('new-branch');
 694 | 
 695 | 				const exists = await gitAdapter.branchExists('new-branch');
 696 | 				expect(exists).toBe(true);
 697 | 			});
 698 | 
 699 | 			it('should throw error if branch already exists', async () => {
 700 | 				await gitAdapter.createBranch('existing-branch');
 701 | 
 702 | 				await expect(
 703 | 					gitAdapter.createBranch('existing-branch')
 704 | 				).rejects.toThrow();
 705 | 			});
 706 | 
 707 | 			it('should not switch to new branch by default', async () => {
 708 | 				await gitAdapter.createBranch('new-branch');
 709 | 
 710 | 				const current = await gitAdapter.getCurrentBranch();
 711 | 				expect(current).toBe('main');
 712 | 			});
 713 | 
 714 | 			it('should throw if working tree is dirty when checkout is requested', async () => {
 715 | 				await fs.writeFile(path.join(testDir, 'dirty.txt'), 'content');
 716 | 
 717 | 				await expect(
 718 | 					gitAdapter.createBranch('new-branch', { checkout: true })
 719 | 				).rejects.toThrow('working tree is not clean');
 720 | 			});
 721 | 		});
 722 | 
 723 | 		describe('checkoutBranch', () => {
 724 | 			it('should checkout existing branch', async () => {
 725 | 				const git = simpleGit(testDir);
 726 | 				await git.checkoutLocalBranch('feature');
 727 | 				await git.checkout('main');
 728 | 
 729 | 				await gitAdapter.checkoutBranch('feature');
 730 | 
 731 | 				const current = await gitAdapter.getCurrentBranch();
 732 | 				expect(current).toBe('feature');
 733 | 			});
 734 | 
 735 | 			it('should throw error for non-existing branch', async () => {
 736 | 				await expect(
 737 | 					gitAdapter.checkoutBranch('nonexistent')
 738 | 				).rejects.toThrow();
 739 | 			});
 740 | 
 741 | 			it('should throw if working tree is dirty', async () => {
 742 | 				const git = simpleGit(testDir);
 743 | 				await git.checkoutLocalBranch('feature');
 744 | 				await git.checkout('main');
 745 | 
 746 | 				await fs.writeFile(path.join(testDir, 'dirty.txt'), 'content');
 747 | 
 748 | 				await expect(gitAdapter.checkoutBranch('feature')).rejects.toThrow(
 749 | 					'working tree is not clean'
 750 | 				);
 751 | 			});
 752 | 
 753 | 			it('should allow force checkout with force flag', async () => {
 754 | 				const git = simpleGit(testDir);
 755 | 				await git.checkoutLocalBranch('feature');
 756 | 				await git.checkout('main');
 757 | 
 758 | 				await fs.writeFile(path.join(testDir, 'dirty.txt'), 'content');
 759 | 
 760 | 				await gitAdapter.checkoutBranch('feature', { force: true });
 761 | 
 762 | 				const current = await gitAdapter.getCurrentBranch();
 763 | 				expect(current).toBe('feature');
 764 | 			});
 765 | 		});
 766 | 
 767 | 		describe('createAndCheckoutBranch', () => {
 768 | 			it('should create and checkout new branch', async () => {
 769 | 				await gitAdapter.createAndCheckoutBranch('new-feature');
 770 | 
 771 | 				const current = await gitAdapter.getCurrentBranch();
 772 | 				expect(current).toBe('new-feature');
 773 | 
 774 | 				const exists = await gitAdapter.branchExists('new-feature');
 775 | 				expect(exists).toBe(true);
 776 | 			});
 777 | 
 778 | 			it('should throw if branch already exists', async () => {
 779 | 				const git = simpleGit(testDir);
 780 | 				await git.checkoutLocalBranch('existing');
 781 | 				await git.checkout('main');
 782 | 
 783 | 				await expect(
 784 | 					gitAdapter.createAndCheckoutBranch('existing')
 785 | 				).rejects.toThrow();
 786 | 			});
 787 | 
 788 | 			it('should throw if working tree is dirty', async () => {
 789 | 				await fs.writeFile(path.join(testDir, 'dirty.txt'), 'content');
 790 | 
 791 | 				await expect(
 792 | 					gitAdapter.createAndCheckoutBranch('new-feature')
 793 | 				).rejects.toThrow('working tree is not clean');
 794 | 			});
 795 | 		});
 796 | 
 797 | 		describe('deleteBranch', () => {
 798 | 			it('should delete existing branch', async () => {
 799 | 				const git = simpleGit(testDir);
 800 | 				await git.checkoutLocalBranch('to-delete');
 801 | 				await git.checkout('main');
 802 | 
 803 | 				await gitAdapter.deleteBranch('to-delete');
 804 | 
 805 | 				const exists = await gitAdapter.branchExists('to-delete');
 806 | 				expect(exists).toBe(false);
 807 | 			});
 808 | 
 809 | 			it('should throw error when deleting current branch', async () => {
 810 | 				await expect(gitAdapter.deleteBranch('main')).rejects.toThrow();
 811 | 			});
 812 | 
 813 | 			it('should throw error for non-existing branch', async () => {
 814 | 				await expect(gitAdapter.deleteBranch('nonexistent')).rejects.toThrow();
 815 | 			});
 816 | 
 817 | 			it('should force delete with force flag', async () => {
 818 | 				const git = simpleGit(testDir);
 819 | 				await git.checkoutLocalBranch('unmerged');
 820 | 				await fs.writeFile(path.join(testDir, 'unmerged.txt'), 'content');
 821 | 				await git.add('unmerged.txt');
 822 | 				await git.commit('Unmerged commit', undefined, {
 823 | 					'--no-gpg-sign': null
 824 | 				});
 825 | 				await git.checkout('main');
 826 | 
 827 | 				await gitAdapter.deleteBranch('unmerged', { force: true });
 828 | 
 829 | 				const exists = await gitAdapter.branchExists('unmerged');
 830 | 				expect(exists).toBe(false);
 831 | 			});
 832 | 		});
 833 | 	});
 834 | 
 835 | 	describe('GitAdapter - Commit Operations', () => {
 836 | 		let testDir;
 837 | 		let gitAdapter;
 838 | 		let simpleGit;
 839 | 
 840 | 		beforeEach(async () => {
 841 | 			testDir = path.join(os.tmpdir(), `git-commit-test-${Date.now()}`);
 842 | 			await fs.ensureDir(testDir);
 843 | 
 844 | 			// Initialize actual git repo with initial commit
 845 | 			simpleGit = (await import('simple-git')).default;
 846 | 			const git = simpleGit(testDir);
 847 | 			await git.init();
 848 | 			await git.addConfig('user.name', 'Test User');
 849 | 			await git.addConfig('user.email', '[email protected]');
 850 | 
 851 | 			// Create initial commit
 852 | 			await fs.writeFile(path.join(testDir, 'README.md'), '# Test Repo');
 853 | 			await git.add('README.md');
 854 | 			await git.commit('Initial commit', undefined, { '--no-gpg-sign': null });
 855 | 
 856 | 			// Rename master to main for consistency
 857 | 			try {
 858 | 				await git.branch(['-m', 'master', 'main']);
 859 | 			} catch (error) {
 860 | 				// Branch might already be main, ignore error
 861 | 			}
 862 | 
 863 | 			gitAdapter = new GitAdapter(testDir);
 864 | 		});
 865 | 
 866 | 		afterEach(async () => {
 867 | 			if (await fs.pathExists(testDir)) {
 868 | 				await fs.remove(testDir);
 869 | 			}
 870 | 		});
 871 | 
 872 | 		describe('stageFiles', () => {
 873 | 			it('should stage single file', async () => {
 874 | 				await fs.writeFile(path.join(testDir, 'new.txt'), 'content');
 875 | 				await gitAdapter.stageFiles(['new.txt']);
 876 | 
 877 | 				const status = await gitAdapter.getStatus();
 878 | 				expect(status.staged).toContain('new.txt');
 879 | 			});
 880 | 
 881 | 			it('should stage multiple files', async () => {
 882 | 				await fs.writeFile(path.join(testDir, 'file1.txt'), 'content1');
 883 | 				await fs.writeFile(path.join(testDir, 'file2.txt'), 'content2');
 884 | 				await gitAdapter.stageFiles(['file1.txt', 'file2.txt']);
 885 | 
 886 | 				const status = await gitAdapter.getStatus();
 887 | 				expect(status.staged).toContain('file1.txt');
 888 | 				expect(status.staged).toContain('file2.txt');
 889 | 			});
 890 | 
 891 | 			it('should stage all files with dot', async () => {
 892 | 				await fs.writeFile(path.join(testDir, 'file1.txt'), 'content1');
 893 | 				await fs.writeFile(path.join(testDir, 'file2.txt'), 'content2');
 894 | 				await gitAdapter.stageFiles(['.']);
 895 | 
 896 | 				const status = await gitAdapter.getStatus();
 897 | 				expect(status.staged.length).toBeGreaterThanOrEqual(2);
 898 | 			});
 899 | 		});
 900 | 
 901 | 		describe('unstageFiles', () => {
 902 | 			it('should unstage single file', async () => {
 903 | 				await fs.writeFile(path.join(testDir, 'staged.txt'), 'content');
 904 | 				const git = simpleGit(testDir);
 905 | 				await git.add('staged.txt');
 906 | 
 907 | 				await gitAdapter.unstageFiles(['staged.txt']);
 908 | 
 909 | 				const status = await gitAdapter.getStatus();
 910 | 				expect(status.staged).not.toContain('staged.txt');
 911 | 			});
 912 | 
 913 | 			it('should unstage multiple files', async () => {
 914 | 				await fs.writeFile(path.join(testDir, 'file1.txt'), 'content1');
 915 | 				await fs.writeFile(path.join(testDir, 'file2.txt'), 'content2');
 916 | 				const git = simpleGit(testDir);
 917 | 				await git.add(['file1.txt', 'file2.txt']);
 918 | 
 919 | 				await gitAdapter.unstageFiles(['file1.txt', 'file2.txt']);
 920 | 
 921 | 				const status = await gitAdapter.getStatus();
 922 | 				expect(status.staged).not.toContain('file1.txt');
 923 | 				expect(status.staged).not.toContain('file2.txt');
 924 | 			});
 925 | 		});
 926 | 
 927 | 		describe('createCommit', () => {
 928 | 			it('should create commit with simple message', async () => {
 929 | 				await fs.writeFile(path.join(testDir, 'new.txt'), 'content');
 930 | 				await gitAdapter.stageFiles(['new.txt']);
 931 | 
 932 | 				await gitAdapter.createCommit('Add new file');
 933 | 
 934 | 				const git = simpleGit(testDir);
 935 | 				const log = await git.log();
 936 | 				expect(log.latest.message).toBe('Add new file');
 937 | 			});
 938 | 
 939 | 			it('should create commit with metadata', async () => {
 940 | 				await fs.writeFile(path.join(testDir, 'new.txt'), 'content');
 941 | 				await gitAdapter.stageFiles(['new.txt']);
 942 | 
 943 | 				const metadata = {
 944 | 					taskId: '2.4',
 945 | 					phase: 'implementation',
 946 | 					timestamp: new Date().toISOString()
 947 | 				};
 948 | 				await gitAdapter.createCommit('Add new file', { metadata });
 949 | 
 950 | 				const commit = await gitAdapter.getLastCommit();
 951 | 				expect(commit.message).toContain('Add new file');
 952 | 				expect(commit.message).toContain('[taskId:2.4]');
 953 | 				expect(commit.message).toContain('[phase:implementation]');
 954 | 			});
 955 | 
 956 | 			it('should throw if no staged changes', async () => {
 957 | 				await expect(gitAdapter.createCommit('Empty commit')).rejects.toThrow();
 958 | 			});
 959 | 
 960 | 			it('should allow empty commits with allowEmpty flag', async () => {
 961 | 				await gitAdapter.createCommit('Empty commit', { allowEmpty: true });
 962 | 
 963 | 				const git = simpleGit(testDir);
 964 | 				const log = await git.log();
 965 | 				expect(log.latest.message).toBe('Empty commit');
 966 | 			});
 967 | 
 968 | 			it('should throw if on default branch without force', async () => {
 969 | 				await fs.writeFile(path.join(testDir, 'new.txt'), 'content');
 970 | 				await gitAdapter.stageFiles(['new.txt']);
 971 | 
 972 | 				await expect(
 973 | 					gitAdapter.createCommit('Add new file', {
 974 | 						enforceNonDefaultBranch: true
 975 | 					})
 976 | 				).rejects.toThrow('cannot commit to default branch');
 977 | 			});
 978 | 
 979 | 			it('should allow commit on default branch with force', async () => {
 980 | 				await fs.writeFile(path.join(testDir, 'new.txt'), 'content');
 981 | 				await gitAdapter.stageFiles(['new.txt']);
 982 | 
 983 | 				await gitAdapter.createCommit('Add new file', {
 984 | 					enforceNonDefaultBranch: true,
 985 | 					force: true
 986 | 				});
 987 | 
 988 | 				const git = simpleGit(testDir);
 989 | 				const log = await git.log();
 990 | 				expect(log.latest.message).toBe('Add new file');
 991 | 			});
 992 | 
 993 | 			it('should allow commit on feature branch with enforcement', async () => {
 994 | 				// Create and checkout feature branch
 995 | 				await gitAdapter.createAndCheckoutBranch('feature-branch');
 996 | 
 997 | 				await fs.writeFile(path.join(testDir, 'new.txt'), 'content');
 998 | 				await gitAdapter.stageFiles(['new.txt']);
 999 | 
1000 | 				await gitAdapter.createCommit('Add new file', {
1001 | 					enforceNonDefaultBranch: true
1002 | 				});
1003 | 
1004 | 				const git = simpleGit(testDir);
1005 | 				const log = await git.log();
1006 | 				expect(log.latest.message).toBe('Add new file');
1007 | 			});
1008 | 		});
1009 | 
1010 | 		describe('getCommitLog', () => {
1011 | 			it('should get recent commits', async () => {
1012 | 				const log = await gitAdapter.getCommitLog();
1013 | 				expect(log.length).toBeGreaterThan(0);
1014 | 				expect(log[0].message.trim()).toBe('Initial commit');
1015 | 			});
1016 | 
1017 | 			it('should limit number of commits', async () => {
1018 | 				// Create additional commits
1019 | 				for (let i = 1; i <= 5; i++) {
1020 | 					await fs.writeFile(path.join(testDir, `file${i}.txt`), `content${i}`);
1021 | 					await gitAdapter.stageFiles([`file${i}.txt`]);
1022 | 					await gitAdapter.createCommit(`Commit ${i}`);
1023 | 				}
1024 | 
1025 | 				const log = await gitAdapter.getCommitLog({ maxCount: 3 });
1026 | 				expect(log.length).toBe(3);
1027 | 			});
1028 | 
1029 | 			it('should return commits with hash and date', async () => {
1030 | 				const log = await gitAdapter.getCommitLog();
1031 | 				expect(log[0]).toHaveProperty('hash');
1032 | 				expect(log[0]).toHaveProperty('date');
1033 | 				expect(log[0]).toHaveProperty('message');
1034 | 				expect(log[0]).toHaveProperty('author_name');
1035 | 				expect(log[0]).toHaveProperty('author_email');
1036 | 			});
1037 | 		});
1038 | 
1039 | 		describe('getLastCommit', () => {
1040 | 			it('should get the last commit', async () => {
1041 | 				await fs.writeFile(path.join(testDir, 'new.txt'), 'content');
1042 | 				await gitAdapter.stageFiles(['new.txt']);
1043 | 				await gitAdapter.createCommit('Latest commit');
1044 | 
1045 | 				const commit = await gitAdapter.getLastCommit();
1046 | 				expect(commit.message.trim()).toBe('Latest commit');
1047 | 			});
1048 | 
1049 | 			it('should return commit with metadata if present', async () => {
1050 | 				await fs.writeFile(path.join(testDir, 'new.txt'), 'content');
1051 | 				await gitAdapter.stageFiles(['new.txt']);
1052 | 				const metadata = { taskId: '2.4' };
1053 | 				await gitAdapter.createCommit('With metadata', { metadata });
1054 | 
1055 | 				const commit = await gitAdapter.getLastCommit();
1056 | 				expect(commit.message).toContain('[taskId:2.4]');
1057 | 			});
1058 | 		});
1059 | 	});
1060 | 
1061 | 	describe('GitAdapter - Default Branch Detection and Protection', () => {
1062 | 		let testDir;
1063 | 		let gitAdapter;
1064 | 		let simpleGit;
1065 | 
1066 | 		beforeEach(async () => {
1067 | 			testDir = path.join(os.tmpdir(), `git-default-branch-test-${Date.now()}`);
1068 | 			await fs.ensureDir(testDir);
1069 | 
1070 | 			// Initialize actual git repo with initial commit
1071 | 			simpleGit = (await import('simple-git')).default;
1072 | 			const git = simpleGit(testDir);
1073 | 			await git.init();
1074 | 			await git.addConfig('user.name', 'Test User');
1075 | 			await git.addConfig('user.email', '[email protected]');
1076 | 
1077 | 			// Create initial commit
1078 | 			await fs.writeFile(path.join(testDir, 'README.md'), '# Test Repo');
1079 | 			await git.add('README.md');
1080 | 			await git.commit('Initial commit', undefined, { '--no-gpg-sign': null });
1081 | 
1082 | 			// Rename master to main for consistency
1083 | 			try {
1084 | 				await git.branch(['-m', 'master', 'main']);
1085 | 			} catch (error) {
1086 | 				// Branch might already be main, ignore error
1087 | 			}
1088 | 
1089 | 			gitAdapter = new GitAdapter(testDir);
1090 | 		});
1091 | 
1092 | 		afterEach(async () => {
1093 | 			if (await fs.pathExists(testDir)) {
1094 | 				await fs.remove(testDir);
1095 | 			}
1096 | 		});
1097 | 
1098 | 		describe('getDefaultBranch', () => {
1099 | 			it('should detect main as default branch', async () => {
1100 | 				const defaultBranch = await gitAdapter.getDefaultBranch();
1101 | 				expect(defaultBranch).toBe('main');
1102 | 			});
1103 | 
1104 | 			it('should detect master if renamed back', async () => {
1105 | 				const git = simpleGit(testDir);
1106 | 				await git.branch(['-m', 'main', 'master']);
1107 | 
1108 | 				const defaultBranch = await gitAdapter.getDefaultBranch();
1109 | 				expect(defaultBranch).toBe('master');
1110 | 			});
1111 | 		});
1112 | 
1113 | 		describe('isDefaultBranch', () => {
1114 | 			it('should return true for main branch', async () => {
1115 | 				const isDefault = await gitAdapter.isDefaultBranch('main');
1116 | 				expect(isDefault).toBe(true);
1117 | 			});
1118 | 
1119 | 			it('should return true for master branch', async () => {
1120 | 				const isDefault = await gitAdapter.isDefaultBranch('master');
1121 | 				expect(isDefault).toBe(true);
1122 | 			});
1123 | 
1124 | 			it('should return true for develop branch', async () => {
1125 | 				const isDefault = await gitAdapter.isDefaultBranch('develop');
1126 | 				expect(isDefault).toBe(true);
1127 | 			});
1128 | 
1129 | 			it('should return false for feature branch', async () => {
1130 | 				const isDefault = await gitAdapter.isDefaultBranch('feature-branch');
1131 | 				expect(isDefault).toBe(false);
1132 | 			});
1133 | 		});
1134 | 
1135 | 		describe('isOnDefaultBranch', () => {
1136 | 			it('should return true when on main', async () => {
1137 | 				const onDefault = await gitAdapter.isOnDefaultBranch();
1138 | 				expect(onDefault).toBe(true);
1139 | 			});
1140 | 
1141 | 			it('should return false when on feature branch', async () => {
1142 | 				await gitAdapter.createAndCheckoutBranch('feature-branch');
1143 | 
1144 | 				const onDefault = await gitAdapter.isOnDefaultBranch();
1145 | 				expect(onDefault).toBe(false);
1146 | 			});
1147 | 		});
1148 | 
1149 | 		describe('ensureNotOnDefaultBranch', () => {
1150 | 			it('should throw when on main branch', async () => {
1151 | 				await expect(gitAdapter.ensureNotOnDefaultBranch()).rejects.toThrow(
1152 | 					'currently on default branch'
1153 | 				);
1154 | 			});
1155 | 
1156 | 			it('should not throw when on feature branch', async () => {
1157 | 				await gitAdapter.createAndCheckoutBranch('feature-branch');
1158 | 
1159 | 				await expect(
1160 | 					gitAdapter.ensureNotOnDefaultBranch()
1161 | 				).resolves.not.toThrow();
1162 | 			});
1163 | 		});
1164 | 	});
1165 | 
1166 | 	describe('GitAdapter - Push Operations', () => {
1167 | 		let testDir;
1168 | 		let gitAdapter;
1169 | 
1170 | 		beforeEach(async () => {
1171 | 			testDir = path.join(os.tmpdir(), `git-push-test-${Date.now()}`);
1172 | 			await fs.ensureDir(testDir);
1173 | 
1174 | 			const simpleGit = (await import('simple-git')).default;
1175 | 			const git = simpleGit(testDir);
1176 | 			await git.init();
1177 | 			await git.addConfig('user.name', 'Test User');
1178 | 			await git.addConfig('user.email', '[email protected]');
1179 | 
1180 | 			await fs.writeFile(path.join(testDir, 'README.md'), '# Test Repo');
1181 | 			await git.add('README.md');
1182 | 			await git.commit('Initial commit', undefined, { '--no-gpg-sign': null });
1183 | 
1184 | 			try {
1185 | 				await git.branch(['-m', 'master', 'main']);
1186 | 			} catch (error) {}
1187 | 
1188 | 			gitAdapter = new GitAdapter(testDir);
1189 | 		});
1190 | 
1191 | 		afterEach(async () => {
1192 | 			if (await fs.pathExists(testDir)) {
1193 | 				await fs.remove(testDir);
1194 | 			}
1195 | 		});
1196 | 
1197 | 		describe('hasRemote', () => {
1198 | 			it('should return false when no remotes exist', async () => {
1199 | 				const hasRemote = await gitAdapter.hasRemote();
1200 | 				expect(hasRemote).toBe(false);
1201 | 			});
1202 | 		});
1203 | 
1204 | 		describe('getRemotes', () => {
1205 | 			it('should return empty array when no remotes', async () => {
1206 | 				const remotes = await gitAdapter.getRemotes();
1207 | 				expect(remotes).toEqual([]);
1208 | 			});
1209 | 		});
1210 | 	});
1211 | });
1212 | 
```

--------------------------------------------------------------------------------
/tests/unit/config-manager.test.js:
--------------------------------------------------------------------------------

```javascript
   1 | import fs from 'fs';
   2 | import path from 'path';
   3 | import { jest } from '@jest/globals';
   4 | import { fileURLToPath } from 'url';
   5 | 
   6 | // Mock modules first before any imports
   7 | jest.mock('fs', () => ({
   8 | 	existsSync: jest.fn((filePath) => {
   9 | 		// Prevent Jest internal file access
  10 | 		if (
  11 | 			filePath.includes('jest-message-util') ||
  12 | 			filePath.includes('node_modules')
  13 | 		) {
  14 | 			return false;
  15 | 		}
  16 | 		return false; // Default to false for config discovery prevention
  17 | 	}),
  18 | 	readFileSync: jest.fn(() => '{}'),
  19 | 	writeFileSync: jest.fn(),
  20 | 	mkdirSync: jest.fn()
  21 | }));
  22 | 
  23 | jest.mock('path', () => ({
  24 | 	join: jest.fn((dir, file) => `${dir}/${file}`),
  25 | 	dirname: jest.fn((filePath) => filePath.split('/').slice(0, -1).join('/')),
  26 | 	resolve: jest.fn((...paths) => paths.join('/')),
  27 | 	basename: jest.fn((filePath) => filePath.split('/').pop())
  28 | }));
  29 | 
  30 | jest.mock('chalk', () => ({
  31 | 	red: jest.fn((text) => text),
  32 | 	blue: jest.fn((text) => text),
  33 | 	green: jest.fn((text) => text),
  34 | 	yellow: jest.fn((text) => text),
  35 | 	white: jest.fn(() => ({
  36 | 		bold: jest.fn((text) => text)
  37 | 	})),
  38 | 	reset: jest.fn((text) => text),
  39 | 	dim: jest.fn((text) => text) // Add dim function to prevent chalk errors
  40 | }));
  41 | 
  42 | // Mock console to prevent Jest internal access
  43 | const mockConsole = {
  44 | 	log: jest.fn(),
  45 | 	info: jest.fn(),
  46 | 	warn: jest.fn(),
  47 | 	error: jest.fn()
  48 | };
  49 | global.console = mockConsole;
  50 | 
  51 | // --- Define Mock Function Instances ---
  52 | const mockFindConfigPath = jest.fn(() => null); // Default to null, can be overridden in tests
  53 | 
  54 | // Mock path-utils to prevent config file path discovery and logging
  55 | jest.mock('../../src/utils/path-utils.js', () => ({
  56 | 	__esModule: true,
  57 | 	findProjectRoot: jest.fn(() => '/mock/project'),
  58 | 	findConfigPath: mockFindConfigPath, // Use the mock function instance
  59 | 	findTasksPath: jest.fn(() => '/mock/tasks.json'),
  60 | 	findComplexityReportPath: jest.fn(() => null),
  61 | 	resolveTasksOutputPath: jest.fn(() => '/mock/tasks.json'),
  62 | 	resolveComplexityReportOutputPath: jest.fn(() => '/mock/report.json')
  63 | }));
  64 | 
  65 | // --- Read REAL supported-models.json data BEFORE mocks ---
  66 | const __filename = fileURLToPath(import.meta.url); // Get current file path
  67 | const __dirname = path.dirname(__filename); // Get current directory
  68 | const realSupportedModelsPath = path.resolve(
  69 | 	__dirname,
  70 | 	'../../scripts/modules/supported-models.json'
  71 | );
  72 | let REAL_SUPPORTED_MODELS_CONTENT;
  73 | let REAL_SUPPORTED_MODELS_DATA;
  74 | try {
  75 | 	REAL_SUPPORTED_MODELS_CONTENT = fs.readFileSync(
  76 | 		realSupportedModelsPath,
  77 | 		'utf-8'
  78 | 	);
  79 | 	REAL_SUPPORTED_MODELS_DATA = JSON.parse(REAL_SUPPORTED_MODELS_CONTENT);
  80 | } catch (err) {
  81 | 	console.error(
  82 | 		'FATAL TEST SETUP ERROR: Could not read or parse real supported-models.json',
  83 | 		err
  84 | 	);
  85 | 	REAL_SUPPORTED_MODELS_CONTENT = '{}'; // Default to empty object on error
  86 | 	REAL_SUPPORTED_MODELS_DATA = {};
  87 | 	process.exit(1); // Exit if essential test data can't be loaded
  88 | }
  89 | 
  90 | // --- Define Mock Function Instances ---
  91 | const mockFindProjectRoot = jest.fn();
  92 | const mockLog = jest.fn();
  93 | 
  94 | // --- Mock Dependencies BEFORE importing the module under test ---
  95 | 
  96 | // Mock the 'utils.js' module using a factory function
  97 | jest.mock('../../scripts/modules/utils.js', () => ({
  98 | 	__esModule: true, // Indicate it's an ES module mock
  99 | 	findProjectRoot: mockFindProjectRoot, // Use the mock function instance
 100 | 	log: mockLog, // Use the mock function instance
 101 | 	// Include other necessary exports from utils if config-manager uses them directly
 102 | 	resolveEnvVariable: jest.fn() // Example if needed
 103 | }));
 104 | 
 105 | // --- Import the module under test AFTER mocks are defined ---
 106 | import * as configManager from '../../scripts/modules/config-manager.js';
 107 | // Import the mocked 'fs' module to allow spying on its functions
 108 | import fsMocked from 'fs';
 109 | 
 110 | // --- Test Data (Keep as is, ensure DEFAULT_CONFIG is accurate) ---
 111 | const MOCK_PROJECT_ROOT = '/mock/project';
 112 | const MOCK_CONFIG_PATH = path.join(
 113 | 	MOCK_PROJECT_ROOT,
 114 | 	'.taskmaster/config.json'
 115 | );
 116 | 
 117 | // Updated DEFAULT_CONFIG reflecting the implementation
 118 | const DEFAULT_CONFIG = {
 119 | 	models: {
 120 | 		main: {
 121 | 			provider: 'anthropic',
 122 | 			modelId: 'claude-sonnet-4-20250514',
 123 | 			maxTokens: 64000,
 124 | 			temperature: 0.2
 125 | 		},
 126 | 		research: {
 127 | 			provider: 'perplexity',
 128 | 			modelId: 'sonar',
 129 | 			maxTokens: 8700,
 130 | 			temperature: 0.1
 131 | 		},
 132 | 		fallback: {
 133 | 			provider: 'anthropic',
 134 | 			modelId: 'claude-3-7-sonnet-20250219',
 135 | 			maxTokens: 120000,
 136 | 			temperature: 0.2
 137 | 		}
 138 | 	},
 139 | 	global: {
 140 | 		logLevel: 'info',
 141 | 		debug: false,
 142 | 		defaultNumTasks: 10,
 143 | 		defaultSubtasks: 5,
 144 | 		defaultPriority: 'medium',
 145 | 		projectName: 'Task Master',
 146 | 		ollamaBaseURL: 'http://localhost:11434/api',
 147 | 		bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com',
 148 | 		enableCodebaseAnalysis: true,
 149 | 		enableProxy: false,
 150 | 		responseLanguage: 'English'
 151 | 	},
 152 | 	claudeCode: {},
 153 | 	codexCli: {},
 154 | 	grokCli: {
 155 | 		timeout: 120000,
 156 | 		workingDirectory: null,
 157 | 		defaultModel: 'grok-4-latest'
 158 | 	}
 159 | };
 160 | 
 161 | // Other test data (VALID_CUSTOM_CONFIG, PARTIAL_CONFIG, INVALID_PROVIDER_CONFIG)
 162 | const VALID_CUSTOM_CONFIG = {
 163 | 	models: {
 164 | 		main: {
 165 | 			provider: 'openai',
 166 | 			modelId: 'gpt-4o',
 167 | 			maxTokens: 4096,
 168 | 			temperature: 0.5
 169 | 		},
 170 | 		research: {
 171 | 			provider: 'google',
 172 | 			modelId: 'gemini-1.5-pro-latest',
 173 | 			maxTokens: 8192,
 174 | 			temperature: 0.3
 175 | 		},
 176 | 		fallback: {
 177 | 			provider: 'anthropic',
 178 | 			modelId: 'claude-3-opus-20240229',
 179 | 			maxTokens: 100000,
 180 | 			temperature: 0.4
 181 | 		}
 182 | 	},
 183 | 	global: {
 184 | 		logLevel: 'debug',
 185 | 		defaultPriority: 'high',
 186 | 		projectName: 'My Custom Project'
 187 | 	}
 188 | };
 189 | 
 190 | const PARTIAL_CONFIG = {
 191 | 	models: {
 192 | 		main: { provider: 'openai', modelId: 'gpt-4-turbo' }
 193 | 	},
 194 | 	global: {
 195 | 		projectName: 'Partial Project'
 196 | 	}
 197 | };
 198 | 
 199 | const INVALID_PROVIDER_CONFIG = {
 200 | 	models: {
 201 | 		main: { provider: 'invalid-provider', modelId: 'some-model' },
 202 | 		research: {
 203 | 			provider: 'perplexity',
 204 | 			modelId: 'llama-3-sonar-large-32k-online'
 205 | 		}
 206 | 	},
 207 | 	global: {
 208 | 		logLevel: 'warn'
 209 | 	}
 210 | };
 211 | 
 212 | // Claude Code test data
 213 | const VALID_CLAUDE_CODE_CONFIG = {
 214 | 	maxTurns: 5,
 215 | 	customSystemPrompt: 'You are a helpful coding assistant',
 216 | 	appendSystemPrompt: 'Always follow best practices',
 217 | 	permissionMode: 'acceptEdits',
 218 | 	allowedTools: ['Read', 'LS', 'Edit'],
 219 | 	disallowedTools: ['Write'],
 220 | 	mcpServers: {
 221 | 		'test-server': {
 222 | 			type: 'stdio',
 223 | 			command: 'node',
 224 | 			args: ['server.js'],
 225 | 			env: { NODE_ENV: 'test' }
 226 | 		}
 227 | 	},
 228 | 	commandSpecific: {
 229 | 		'add-task': {
 230 | 			maxTurns: 3,
 231 | 			permissionMode: 'plan'
 232 | 		},
 233 | 		research: {
 234 | 			customSystemPrompt: 'You are a research assistant'
 235 | 		}
 236 | 	}
 237 | };
 238 | 
 239 | const INVALID_CLAUDE_CODE_CONFIG = {
 240 | 	maxTurns: 'invalid', // Should be number
 241 | 	permissionMode: 'invalid-mode', // Invalid enum value
 242 | 	allowedTools: 'not-an-array', // Should be array
 243 | 	mcpServers: {
 244 | 		'invalid-server': {
 245 | 			type: 'invalid-type', // Invalid enum value
 246 | 			url: 'not-a-valid-url' // Invalid URL format
 247 | 		}
 248 | 	},
 249 | 	commandSpecific: {
 250 | 		'invalid-command': {
 251 | 			// Invalid command name
 252 | 			maxTurns: -1 // Invalid negative number
 253 | 		}
 254 | 	}
 255 | };
 256 | 
 257 | const PARTIAL_CLAUDE_CODE_CONFIG = {
 258 | 	maxTurns: 10,
 259 | 	permissionMode: 'default',
 260 | 	commandSpecific: {
 261 | 		'expand-task': {
 262 | 			customSystemPrompt: 'Focus on task breakdown'
 263 | 		}
 264 | 	}
 265 | };
 266 | 
 267 | // Define spies globally to be restored in afterAll
 268 | let consoleErrorSpy;
 269 | let consoleWarnSpy;
 270 | let fsReadFileSyncSpy;
 271 | let fsWriteFileSyncSpy;
 272 | let fsExistsSyncSpy;
 273 | 
 274 | beforeAll(() => {
 275 | 	// Set up console spies
 276 | 	consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
 277 | 	consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
 278 | });
 279 | 
 280 | afterAll(() => {
 281 | 	// Restore all spies
 282 | 	jest.restoreAllMocks();
 283 | });
 284 | 
 285 | // Reset mocks before each test for isolation
 286 | beforeEach(() => {
 287 | 	// Clear all mock calls and reset implementations between tests
 288 | 	jest.clearAllMocks();
 289 | 	// Reset the external mock instances for utils
 290 | 	mockFindProjectRoot.mockReset();
 291 | 	mockLog.mockReset();
 292 | 	mockFindConfigPath.mockReset();
 293 | 
 294 | 	// --- Set up spies ON the imported 'fs' mock ---
 295 | 	fsExistsSyncSpy = jest.spyOn(fsMocked, 'existsSync');
 296 | 	fsReadFileSyncSpy = jest.spyOn(fsMocked, 'readFileSync');
 297 | 	fsWriteFileSyncSpy = jest.spyOn(fsMocked, 'writeFileSync');
 298 | 
 299 | 	// --- Default Mock Implementations ---
 300 | 	mockFindProjectRoot.mockReturnValue(MOCK_PROJECT_ROOT); // Default for utils.findProjectRoot
 301 | 	mockFindConfigPath.mockReturnValue(null); // Default to no config file found
 302 | 	fsExistsSyncSpy.mockReturnValue(true); // Assume files exist by default
 303 | 
 304 | 	// Default readFileSync: Return REAL models content, mocked config, or throw error
 305 | 	fsReadFileSyncSpy.mockImplementation((filePath) => {
 306 | 		const baseName = path.basename(filePath);
 307 | 		if (baseName === 'supported-models.json') {
 308 | 			// Return the REAL file content stringified
 309 | 			return REAL_SUPPORTED_MODELS_CONTENT;
 310 | 		} else if (filePath === MOCK_CONFIG_PATH) {
 311 | 			// Still mock the .taskmasterconfig reads
 312 | 			return JSON.stringify(DEFAULT_CONFIG); // Default behavior
 313 | 		}
 314 | 		// For Jest internal files or other unexpected files, return empty string instead of throwing
 315 | 		// This prevents Jest's internal file operations from breaking tests
 316 | 		if (
 317 | 			filePath.includes('jest-message-util') ||
 318 | 			filePath.includes('node_modules')
 319 | 		) {
 320 | 			return '{}'; // Return empty JSON for Jest internal files
 321 | 		}
 322 | 		// Throw for truly unexpected reads that should be caught in tests
 323 | 		throw new Error(`Unexpected fs.readFileSync call in test: ${filePath}`);
 324 | 	});
 325 | 
 326 | 	// Default writeFileSync: Do nothing, just allow calls
 327 | 	fsWriteFileSyncSpy.mockImplementation(() => {});
 328 | });
 329 | 
 330 | // --- Validation Functions ---
 331 | describe('Validation Functions', () => {
 332 | 	// Tests for validateProvider and validateProviderModelCombination
 333 | 	test('validateProvider should return true for valid providers', () => {
 334 | 		expect(configManager.validateProvider('openai')).toBe(true);
 335 | 		expect(configManager.validateProvider('anthropic')).toBe(true);
 336 | 		expect(configManager.validateProvider('google')).toBe(true);
 337 | 		expect(configManager.validateProvider('perplexity')).toBe(true);
 338 | 		expect(configManager.validateProvider('ollama')).toBe(true);
 339 | 		expect(configManager.validateProvider('openrouter')).toBe(true);
 340 | 		expect(configManager.validateProvider('bedrock')).toBe(true);
 341 | 	});
 342 | 
 343 | 	test('validateProvider should return false for invalid providers', () => {
 344 | 		expect(configManager.validateProvider('invalid-provider')).toBe(false);
 345 | 		expect(configManager.validateProvider('grok')).toBe(false); // Not in mock map
 346 | 		expect(configManager.validateProvider('')).toBe(false);
 347 | 		expect(configManager.validateProvider(null)).toBe(false);
 348 | 	});
 349 | 
 350 | 	test('validateProviderModelCombination should validate known good combinations', () => {
 351 | 		// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
 352 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
 353 | 		expect(
 354 | 			configManager.validateProviderModelCombination('openai', 'gpt-4o')
 355 | 		).toBe(true);
 356 | 		expect(
 357 | 			configManager.validateProviderModelCombination(
 358 | 				'anthropic',
 359 | 				'claude-3-5-sonnet-20241022'
 360 | 			)
 361 | 		).toBe(true);
 362 | 	});
 363 | 
 364 | 	test('validateProviderModelCombination should return false for known bad combinations', () => {
 365 | 		// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
 366 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
 367 | 		expect(
 368 | 			configManager.validateProviderModelCombination(
 369 | 				'openai',
 370 | 				'claude-3-opus-20240229'
 371 | 			)
 372 | 		).toBe(false);
 373 | 	});
 374 | 
 375 | 	test('validateProviderModelCombination should return true for ollama/openrouter (empty lists in map)', () => {
 376 | 		// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
 377 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
 378 | 		expect(
 379 | 			configManager.validateProviderModelCombination('ollama', 'any-model')
 380 | 		).toBe(false);
 381 | 		expect(
 382 | 			configManager.validateProviderModelCombination('openrouter', 'any/model')
 383 | 		).toBe(false);
 384 | 	});
 385 | 
 386 | 	test('validateProviderModelCombination should return true for providers not in map', () => {
 387 | 		// Re-load config to ensure MODEL_MAP is populated from mock (now real data)
 388 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
 389 | 		// The implementation returns true if the provider isn't in the map
 390 | 		expect(
 391 | 			configManager.validateProviderModelCombination(
 392 | 				'unknown-provider',
 393 | 				'some-model'
 394 | 			)
 395 | 		).toBe(true);
 396 | 	});
 397 | });
 398 | 
 399 | // --- Claude Code Validation Tests ---
 400 | describe('Claude Code Validation', () => {
 401 | 	test('validateClaudeCodeSettings should return valid settings for correct input', () => {
 402 | 		const result = configManager.validateClaudeCodeSettings(
 403 | 			VALID_CLAUDE_CODE_CONFIG
 404 | 		);
 405 | 
 406 | 		expect(result).toEqual(VALID_CLAUDE_CODE_CONFIG);
 407 | 		expect(consoleWarnSpy).not.toHaveBeenCalled();
 408 | 	});
 409 | 
 410 | 	test('validateClaudeCodeSettings should return empty object for invalid input', () => {
 411 | 		const result = configManager.validateClaudeCodeSettings(
 412 | 			INVALID_CLAUDE_CODE_CONFIG
 413 | 		);
 414 | 
 415 | 		expect(result).toEqual({});
 416 | 		expect(consoleWarnSpy).toHaveBeenCalledWith(
 417 | 			expect.stringContaining('Warning: Invalid Claude Code settings in config')
 418 | 		);
 419 | 	});
 420 | 
 421 | 	test('validateClaudeCodeSettings should handle partial valid configuration', () => {
 422 | 		const result = configManager.validateClaudeCodeSettings(
 423 | 			PARTIAL_CLAUDE_CODE_CONFIG
 424 | 		);
 425 | 
 426 | 		expect(result).toEqual(PARTIAL_CLAUDE_CODE_CONFIG);
 427 | 		expect(consoleWarnSpy).not.toHaveBeenCalled();
 428 | 	});
 429 | 
 430 | 	test('validateClaudeCodeSettings should return empty object for empty input', () => {
 431 | 		const result = configManager.validateClaudeCodeSettings({});
 432 | 
 433 | 		expect(result).toEqual({});
 434 | 		expect(consoleWarnSpy).not.toHaveBeenCalled();
 435 | 	});
 436 | 
 437 | 	test('validateClaudeCodeSettings should handle null/undefined input', () => {
 438 | 		expect(configManager.validateClaudeCodeSettings(null)).toEqual({});
 439 | 		expect(configManager.validateClaudeCodeSettings(undefined)).toEqual({});
 440 | 		expect(consoleWarnSpy).toHaveBeenCalledTimes(2);
 441 | 	});
 442 | });
 443 | 
 444 | // --- Claude Code Getter Tests ---
 445 | describe('Claude Code Getter Functions', () => {
 446 | 	test('getClaudeCodeSettings should return default empty object when no config exists', () => {
 447 | 		// No config file exists, should return empty object
 448 | 		fsExistsSyncSpy.mockReturnValue(false);
 449 | 		const settings = configManager.getClaudeCodeSettings(MOCK_PROJECT_ROOT);
 450 | 
 451 | 		expect(settings).toEqual({});
 452 | 	});
 453 | 
 454 | 	test('getClaudeCodeSettings should return merged settings from config file', () => {
 455 | 		// Config file with Claude Code settings
 456 | 		const configWithClaudeCode = {
 457 | 			...VALID_CUSTOM_CONFIG,
 458 | 			claudeCode: VALID_CLAUDE_CODE_CONFIG
 459 | 		};
 460 | 
 461 | 		// Mock findConfigPath to return the mock config path
 462 | 		mockFindConfigPath.mockReturnValue(MOCK_CONFIG_PATH);
 463 | 
 464 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 465 | 			if (filePath === MOCK_CONFIG_PATH)
 466 | 				return JSON.stringify(configWithClaudeCode);
 467 | 			if (path.basename(filePath) === 'supported-models.json') {
 468 | 				return JSON.stringify({
 469 | 					openai: [{ id: 'gpt-4o' }],
 470 | 					google: [{ id: 'gemini-1.5-pro-latest' }],
 471 | 					anthropic: [
 472 | 						{ id: 'claude-3-opus-20240229' },
 473 | 						{ id: 'claude-3-7-sonnet-20250219' },
 474 | 						{ id: 'claude-3-5-sonnet' }
 475 | 					],
 476 | 					perplexity: [{ id: 'sonar-pro' }],
 477 | 					ollama: [],
 478 | 					openrouter: []
 479 | 				});
 480 | 			}
 481 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 482 | 		});
 483 | 		fsExistsSyncSpy.mockReturnValue(true);
 484 | 
 485 | 		const settings = configManager.getClaudeCodeSettings(
 486 | 			MOCK_PROJECT_ROOT,
 487 | 			true
 488 | 		); // Force reload
 489 | 
 490 | 		expect(settings).toEqual(VALID_CLAUDE_CODE_CONFIG);
 491 | 	});
 492 | 
 493 | 	test('getClaudeCodeSettingsForCommand should return command-specific settings', () => {
 494 | 		// Config with command-specific settings
 495 | 		const configWithClaudeCode = {
 496 | 			...VALID_CUSTOM_CONFIG,
 497 | 			claudeCode: VALID_CLAUDE_CODE_CONFIG
 498 | 		};
 499 | 
 500 | 		// Mock findConfigPath to return the mock config path
 501 | 		mockFindConfigPath.mockReturnValue(MOCK_CONFIG_PATH);
 502 | 
 503 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 504 | 			if (path.basename(filePath) === 'supported-models.json') return '{}';
 505 | 			if (filePath === MOCK_CONFIG_PATH)
 506 | 				return JSON.stringify(configWithClaudeCode);
 507 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 508 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 509 | 		});
 510 | 		fsExistsSyncSpy.mockReturnValue(true);
 511 | 
 512 | 		const settings = configManager.getClaudeCodeSettingsForCommand(
 513 | 			'add-task',
 514 | 			MOCK_PROJECT_ROOT,
 515 | 			true
 516 | 		); // Force reload
 517 | 
 518 | 		// Should merge global settings with command-specific settings
 519 | 		const expectedSettings = {
 520 | 			...VALID_CLAUDE_CODE_CONFIG,
 521 | 			...VALID_CLAUDE_CODE_CONFIG.commandSpecific['add-task']
 522 | 		};
 523 | 		expect(settings).toEqual(expectedSettings);
 524 | 	});
 525 | 
 526 | 	test('getClaudeCodeSettingsForCommand should return global settings for unknown command', () => {
 527 | 		// Config with Claude Code settings
 528 | 		const configWithClaudeCode = {
 529 | 			...VALID_CUSTOM_CONFIG,
 530 | 			claudeCode: PARTIAL_CLAUDE_CODE_CONFIG
 531 | 		};
 532 | 
 533 | 		// Mock findConfigPath to return the mock config path
 534 | 		mockFindConfigPath.mockReturnValue(MOCK_CONFIG_PATH);
 535 | 
 536 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 537 | 			if (path.basename(filePath) === 'supported-models.json') return '{}';
 538 | 			if (filePath === MOCK_CONFIG_PATH)
 539 | 				return JSON.stringify(configWithClaudeCode);
 540 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 541 | 		});
 542 | 		fsExistsSyncSpy.mockReturnValue(true);
 543 | 
 544 | 		const settings = configManager.getClaudeCodeSettingsForCommand(
 545 | 			'unknown-command',
 546 | 			MOCK_PROJECT_ROOT,
 547 | 			true
 548 | 		); // Force reload
 549 | 
 550 | 		// Should return global settings only
 551 | 		expect(settings).toEqual(PARTIAL_CLAUDE_CODE_CONFIG);
 552 | 	});
 553 | });
 554 | 
 555 | // --- getConfig Tests ---
 556 | describe('getConfig Tests', () => {
 557 | 	test('should return default config if .taskmasterconfig does not exist', () => {
 558 | 		// Arrange
 559 | 		fsExistsSyncSpy.mockReturnValue(false);
 560 | 		// findProjectRoot mock is set in beforeEach
 561 | 
 562 | 		// Act: Call getConfig with explicit root
 563 | 		const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Force reload
 564 | 
 565 | 		// Assert
 566 | 		expect(config).toEqual(DEFAULT_CONFIG);
 567 | 		expect(mockFindProjectRoot).not.toHaveBeenCalled(); // Explicit root provided
 568 | 		// The implementation checks for .taskmaster directory first
 569 | 		expect(fsExistsSyncSpy).toHaveBeenCalledWith(
 570 | 			path.join(MOCK_PROJECT_ROOT, '.taskmaster')
 571 | 		);
 572 | 		expect(fsReadFileSyncSpy).not.toHaveBeenCalled(); // No read if file doesn't exist
 573 | 		expect(consoleWarnSpy).toHaveBeenCalledWith(
 574 | 			expect.stringContaining('not found at provided project root')
 575 | 		);
 576 | 	});
 577 | 
 578 | 	test.skip('should use findProjectRoot and return defaults if file not found', () => {
 579 | 		// TODO: Fix mock interaction, findProjectRoot isn't being registered as called
 580 | 		// Arrange
 581 | 		fsExistsSyncSpy.mockReturnValue(false);
 582 | 		// findProjectRoot mock is set in beforeEach
 583 | 
 584 | 		// Act: Call getConfig without explicit root
 585 | 		const config = configManager.getConfig(null, true); // Force reload
 586 | 
 587 | 		// Assert
 588 | 		expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now
 589 | 		expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
 590 | 		expect(config).toEqual(DEFAULT_CONFIG);
 591 | 		expect(fsReadFileSyncSpy).not.toHaveBeenCalled();
 592 | 		expect(consoleWarnSpy).toHaveBeenCalledWith(
 593 | 			expect.stringContaining('not found at derived root')
 594 | 		); // Adjusted expected warning
 595 | 	});
 596 | 
 597 | 	test('should read and merge valid config file with defaults', () => {
 598 | 		// Arrange: Override readFileSync for this test
 599 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 600 | 			if (filePath === MOCK_CONFIG_PATH)
 601 | 				return JSON.stringify(VALID_CUSTOM_CONFIG);
 602 | 			if (path.basename(filePath) === 'supported-models.json') {
 603 | 				// Provide necessary models for validation within getConfig
 604 | 				return JSON.stringify({
 605 | 					openai: [{ id: 'gpt-4o' }],
 606 | 					google: [{ id: 'gemini-1.5-pro-latest' }],
 607 | 					perplexity: [{ id: 'sonar-pro' }],
 608 | 					anthropic: [
 609 | 						{ id: 'claude-3-opus-20240229' },
 610 | 						{ id: 'claude-3-5-sonnet' },
 611 | 						{ id: 'claude-3-7-sonnet-20250219' },
 612 | 						{ id: 'claude-3-5-sonnet' }
 613 | 					],
 614 | 					ollama: [],
 615 | 					openrouter: []
 616 | 				});
 617 | 			}
 618 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 619 | 		});
 620 | 		fsExistsSyncSpy.mockReturnValue(true);
 621 | 		// findProjectRoot mock set in beforeEach
 622 | 
 623 | 		// Act
 624 | 		const config = configManager.getConfig(MOCK_PROJECT_ROOT, true); // Force reload
 625 | 
 626 | 		// Assert: Construct expected merged config
 627 | 		const expectedMergedConfig = {
 628 | 			models: {
 629 | 				main: {
 630 | 					...DEFAULT_CONFIG.models.main,
 631 | 					...VALID_CUSTOM_CONFIG.models.main
 632 | 				},
 633 | 				research: {
 634 | 					...DEFAULT_CONFIG.models.research,
 635 | 					...VALID_CUSTOM_CONFIG.models.research
 636 | 				},
 637 | 				fallback: {
 638 | 					...DEFAULT_CONFIG.models.fallback,
 639 | 					...VALID_CUSTOM_CONFIG.models.fallback
 640 | 				}
 641 | 			},
 642 | 			global: { ...DEFAULT_CONFIG.global, ...VALID_CUSTOM_CONFIG.global },
 643 | 			claudeCode: {
 644 | 				...DEFAULT_CONFIG.claudeCode,
 645 | 				...VALID_CUSTOM_CONFIG.claudeCode
 646 | 			},
 647 | 			grokCli: { ...DEFAULT_CONFIG.grokCli },
 648 | 			codexCli: { ...DEFAULT_CONFIG.codexCli }
 649 | 		};
 650 | 		expect(config).toEqual(expectedMergedConfig);
 651 | 		expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
 652 | 		expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
 653 | 	});
 654 | 
 655 | 	test('should merge defaults for partial config file', () => {
 656 | 		// Arrange
 657 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 658 | 			if (filePath === MOCK_CONFIG_PATH) return JSON.stringify(PARTIAL_CONFIG);
 659 | 			if (path.basename(filePath) === 'supported-models.json') {
 660 | 				return JSON.stringify({
 661 | 					openai: [{ id: 'gpt-4-turbo' }],
 662 | 					perplexity: [{ id: 'sonar-pro' }],
 663 | 					anthropic: [
 664 | 						{ id: 'claude-3-7-sonnet-20250219' },
 665 | 						{ id: 'claude-3-5-sonnet' }
 666 | 					],
 667 | 					ollama: [],
 668 | 					openrouter: []
 669 | 				});
 670 | 			}
 671 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 672 | 		});
 673 | 		fsExistsSyncSpy.mockReturnValue(true);
 674 | 		// findProjectRoot mock set in beforeEach
 675 | 
 676 | 		// Act
 677 | 		const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
 678 | 
 679 | 		// Assert: Construct expected merged config
 680 | 		const expectedMergedConfig = {
 681 | 			models: {
 682 | 				main: { ...DEFAULT_CONFIG.models.main, ...PARTIAL_CONFIG.models.main },
 683 | 				research: { ...DEFAULT_CONFIG.models.research },
 684 | 				fallback: { ...DEFAULT_CONFIG.models.fallback }
 685 | 			},
 686 | 			global: { ...DEFAULT_CONFIG.global, ...PARTIAL_CONFIG.global },
 687 | 			claudeCode: {
 688 | 				...DEFAULT_CONFIG.claudeCode,
 689 | 				...VALID_CUSTOM_CONFIG.claudeCode
 690 | 			},
 691 | 			grokCli: { ...DEFAULT_CONFIG.grokCli },
 692 | 			codexCli: { ...DEFAULT_CONFIG.codexCli }
 693 | 		};
 694 | 		expect(config).toEqual(expectedMergedConfig);
 695 | 		expect(fsReadFileSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH, 'utf-8');
 696 | 	});
 697 | 
 698 | 	test('should handle JSON parsing error and return defaults', () => {
 699 | 		// Arrange
 700 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 701 | 			if (filePath === MOCK_CONFIG_PATH) return 'invalid json';
 702 | 			// Mock models read needed for initial load before parse error
 703 | 			if (path.basename(filePath) === 'supported-models.json') {
 704 | 				return JSON.stringify({
 705 | 					anthropic: [{ id: 'claude-3-7-sonnet-20250219' }],
 706 | 					perplexity: [{ id: 'sonar-pro' }],
 707 | 					fallback: [{ id: 'claude-3-5-sonnet' }],
 708 | 					ollama: [],
 709 | 					openrouter: []
 710 | 				});
 711 | 			}
 712 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 713 | 		});
 714 | 		fsExistsSyncSpy.mockReturnValue(true);
 715 | 		// findProjectRoot mock set in beforeEach
 716 | 
 717 | 		// Act
 718 | 		const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
 719 | 
 720 | 		// Assert
 721 | 		expect(config).toEqual(DEFAULT_CONFIG);
 722 | 		expect(consoleErrorSpy).toHaveBeenCalledWith(
 723 | 			expect.stringContaining('Error reading or parsing')
 724 | 		);
 725 | 	});
 726 | 
 727 | 	test('should handle file read error and return defaults', () => {
 728 | 		// Arrange
 729 | 		const readError = new Error('Permission denied');
 730 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 731 | 			if (filePath === MOCK_CONFIG_PATH) throw readError;
 732 | 			// Mock models read needed for initial load before read error
 733 | 			if (path.basename(filePath) === 'supported-models.json') {
 734 | 				return JSON.stringify({
 735 | 					anthropic: [{ id: 'claude-3-7-sonnet-20250219' }],
 736 | 					perplexity: [{ id: 'sonar-pro' }],
 737 | 					fallback: [{ id: 'claude-3-5-sonnet' }],
 738 | 					ollama: [],
 739 | 					openrouter: []
 740 | 				});
 741 | 			}
 742 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 743 | 		});
 744 | 		fsExistsSyncSpy.mockReturnValue(true);
 745 | 		// findProjectRoot mock set in beforeEach
 746 | 
 747 | 		// Act
 748 | 		const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
 749 | 
 750 | 		// Assert
 751 | 		expect(config).toEqual(DEFAULT_CONFIG);
 752 | 		expect(consoleErrorSpy).toHaveBeenCalledWith(
 753 | 			expect.stringContaining('Permission denied. Using default configuration.')
 754 | 		);
 755 | 	});
 756 | 
 757 | 	test('should validate provider and fallback to default if invalid', () => {
 758 | 		// Arrange
 759 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 760 | 			if (filePath === MOCK_CONFIG_PATH)
 761 | 				return JSON.stringify(INVALID_PROVIDER_CONFIG);
 762 | 			if (path.basename(filePath) === 'supported-models.json') {
 763 | 				return JSON.stringify({
 764 | 					perplexity: [{ id: 'llama-3-sonar-large-32k-online' }],
 765 | 					anthropic: [
 766 | 						{ id: 'claude-3-7-sonnet-20250219' },
 767 | 						{ id: 'claude-3-5-sonnet' }
 768 | 					],
 769 | 					ollama: [],
 770 | 					openrouter: []
 771 | 				});
 772 | 			}
 773 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 774 | 		});
 775 | 		fsExistsSyncSpy.mockReturnValue(true);
 776 | 		// findProjectRoot mock set in beforeEach
 777 | 
 778 | 		// Act
 779 | 		const config = configManager.getConfig(MOCK_PROJECT_ROOT, true);
 780 | 
 781 | 		// Assert
 782 | 		expect(consoleWarnSpy).toHaveBeenCalledWith(
 783 | 			expect.stringContaining(
 784 | 				'Warning: Invalid main provider "invalid-provider"'
 785 | 			)
 786 | 		);
 787 | 		const expectedMergedConfig = {
 788 | 			models: {
 789 | 				main: { ...DEFAULT_CONFIG.models.main },
 790 | 				research: {
 791 | 					...DEFAULT_CONFIG.models.research,
 792 | 					...INVALID_PROVIDER_CONFIG.models.research
 793 | 				},
 794 | 				fallback: { ...DEFAULT_CONFIG.models.fallback }
 795 | 			},
 796 | 			global: { ...DEFAULT_CONFIG.global, ...INVALID_PROVIDER_CONFIG.global },
 797 | 			claudeCode: {
 798 | 				...DEFAULT_CONFIG.claudeCode,
 799 | 				...VALID_CUSTOM_CONFIG.claudeCode
 800 | 			},
 801 | 			grokCli: { ...DEFAULT_CONFIG.grokCli },
 802 | 			codexCli: { ...DEFAULT_CONFIG.codexCli }
 803 | 		};
 804 | 		expect(config).toEqual(expectedMergedConfig);
 805 | 	});
 806 | });
 807 | 
 808 | // --- writeConfig Tests ---
 809 | describe('writeConfig', () => {
 810 | 	test('should write valid config to file', () => {
 811 | 		// Arrange (Default mocks are sufficient)
 812 | 		// findProjectRoot mock set in beforeEach
 813 | 		fsWriteFileSyncSpy.mockImplementation(() => {}); // Ensure it doesn't throw
 814 | 
 815 | 		// Act
 816 | 		const success = configManager.writeConfig(
 817 | 			VALID_CUSTOM_CONFIG,
 818 | 			MOCK_PROJECT_ROOT
 819 | 		);
 820 | 
 821 | 		// Assert
 822 | 		expect(success).toBe(true);
 823 | 		expect(fsWriteFileSyncSpy).toHaveBeenCalledWith(
 824 | 			MOCK_CONFIG_PATH,
 825 | 			JSON.stringify(VALID_CUSTOM_CONFIG, null, 2) // writeConfig stringifies
 826 | 		);
 827 | 		expect(consoleErrorSpy).not.toHaveBeenCalled();
 828 | 	});
 829 | 
 830 | 	test('should return false and log error if write fails', () => {
 831 | 		// Arrange
 832 | 		const mockWriteError = new Error('Disk full');
 833 | 		fsWriteFileSyncSpy.mockImplementation(() => {
 834 | 			throw mockWriteError;
 835 | 		});
 836 | 		// findProjectRoot mock set in beforeEach
 837 | 
 838 | 		// Act
 839 | 		const success = configManager.writeConfig(
 840 | 			VALID_CUSTOM_CONFIG,
 841 | 			MOCK_PROJECT_ROOT
 842 | 		);
 843 | 
 844 | 		// Assert
 845 | 		expect(success).toBe(false);
 846 | 		expect(fsWriteFileSyncSpy).toHaveBeenCalled();
 847 | 		expect(consoleErrorSpy).toHaveBeenCalledWith(
 848 | 			expect.stringContaining('Disk full')
 849 | 		);
 850 | 	});
 851 | 
 852 | 	test.skip('should return false if project root cannot be determined', () => {
 853 | 		// TODO: Fix mock interaction or function logic, returns true unexpectedly in test
 854 | 		// Arrange: Override mock for this specific test
 855 | 		mockFindProjectRoot.mockReturnValue(null);
 856 | 
 857 | 		// Act: Call without explicit root
 858 | 		const success = configManager.writeConfig(VALID_CUSTOM_CONFIG);
 859 | 
 860 | 		// Assert
 861 | 		expect(success).toBe(false); // Function should return false if root is null
 862 | 		expect(mockFindProjectRoot).toHaveBeenCalled();
 863 | 		expect(fsWriteFileSyncSpy).not.toHaveBeenCalled();
 864 | 		expect(consoleErrorSpy).toHaveBeenCalledWith(
 865 | 			expect.stringContaining('Could not determine project root')
 866 | 		);
 867 | 	});
 868 | });
 869 | 
 870 | // --- Getter Functions ---
 871 | describe('Getter Functions', () => {
 872 | 	test('getMainProvider should return provider from config', () => {
 873 | 		// Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG
 874 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 875 | 			if (filePath === MOCK_CONFIG_PATH)
 876 | 				return JSON.stringify(VALID_CUSTOM_CONFIG);
 877 | 			if (path.basename(filePath) === 'supported-models.json') {
 878 | 				return JSON.stringify({
 879 | 					openai: [{ id: 'gpt-4o' }],
 880 | 					google: [{ id: 'gemini-1.5-pro-latest' }],
 881 | 					anthropic: [
 882 | 						{ id: 'claude-3-opus-20240229' },
 883 | 						{ id: 'claude-3-7-sonnet-20250219' },
 884 | 						{ id: 'claude-3-5-sonnet' }
 885 | 					],
 886 | 					perplexity: [{ id: 'sonar-pro' }],
 887 | 					ollama: [],
 888 | 					openrouter: []
 889 | 				}); // Added perplexity
 890 | 			}
 891 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 892 | 		});
 893 | 		fsExistsSyncSpy.mockReturnValue(true);
 894 | 		// findProjectRoot mock set in beforeEach
 895 | 
 896 | 		// Act
 897 | 		const provider = configManager.getMainProvider(MOCK_PROJECT_ROOT);
 898 | 
 899 | 		// Assert
 900 | 		expect(provider).toBe(VALID_CUSTOM_CONFIG.models.main.provider);
 901 | 	});
 902 | 
 903 | 	test('getLogLevel should return logLevel from config', () => {
 904 | 		// Arrange: Set up readFileSync to return VALID_CUSTOM_CONFIG
 905 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 906 | 			if (filePath === MOCK_CONFIG_PATH)
 907 | 				return JSON.stringify(VALID_CUSTOM_CONFIG);
 908 | 			if (path.basename(filePath) === 'supported-models.json') {
 909 | 				// Provide enough mock model data for validation within getConfig
 910 | 				return JSON.stringify({
 911 | 					openai: [{ id: 'gpt-4o' }],
 912 | 					google: [{ id: 'gemini-1.5-pro-latest' }],
 913 | 					anthropic: [
 914 | 						{ id: 'claude-3-opus-20240229' },
 915 | 						{ id: 'claude-3-7-sonnet-20250219' },
 916 | 						{ id: 'claude-3-5-sonnet' }
 917 | 					],
 918 | 					perplexity: [{ id: 'sonar-pro' }],
 919 | 					ollama: [],
 920 | 					openrouter: []
 921 | 				});
 922 | 			}
 923 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 924 | 		});
 925 | 		fsExistsSyncSpy.mockReturnValue(true);
 926 | 		// findProjectRoot mock set in beforeEach
 927 | 
 928 | 		// Act
 929 | 		const logLevel = configManager.getLogLevel(MOCK_PROJECT_ROOT);
 930 | 
 931 | 		// Assert
 932 | 		expect(logLevel).toBe(VALID_CUSTOM_CONFIG.global.logLevel);
 933 | 	});
 934 | 
 935 | 	test('getResponseLanguage should return responseLanguage from config', () => {
 936 | 		// Arrange
 937 | 		// Prepare a config object with responseLanguage property for this test
 938 | 		const configWithLanguage = JSON.stringify({
 939 | 			models: {
 940 | 				main: { provider: 'openai', modelId: 'gpt-4-turbo' }
 941 | 			},
 942 | 			global: {
 943 | 				projectName: 'Test Project',
 944 | 				responseLanguage: '中文'
 945 | 			}
 946 | 		});
 947 | 
 948 | 		// Set up fs.readFileSync to return our test config
 949 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 950 | 			if (filePath === MOCK_CONFIG_PATH) {
 951 | 				return configWithLanguage;
 952 | 			}
 953 | 			if (path.basename(filePath) === 'supported-models.json') {
 954 | 				return JSON.stringify({
 955 | 					openai: [{ id: 'gpt-4-turbo' }]
 956 | 				});
 957 | 			}
 958 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 959 | 		});
 960 | 
 961 | 		fsExistsSyncSpy.mockReturnValue(true);
 962 | 
 963 | 		// Ensure getConfig returns new values instead of cached ones
 964 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
 965 | 
 966 | 		// Act
 967 | 		const responseLanguage =
 968 | 			configManager.getResponseLanguage(MOCK_PROJECT_ROOT);
 969 | 
 970 | 		// Assert
 971 | 		expect(responseLanguage).toBe('中文');
 972 | 	});
 973 | 
 974 | 	test('getResponseLanguage should return undefined when responseLanguage is not in config', () => {
 975 | 		// Arrange
 976 | 		const configWithoutLanguage = JSON.stringify({
 977 | 			models: {
 978 | 				main: { provider: 'openai', modelId: 'gpt-4-turbo' }
 979 | 			},
 980 | 			global: {
 981 | 				projectName: 'Test Project'
 982 | 				// No responseLanguage property
 983 | 			}
 984 | 		});
 985 | 
 986 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
 987 | 			if (filePath === MOCK_CONFIG_PATH) {
 988 | 				return configWithoutLanguage;
 989 | 			}
 990 | 			if (path.basename(filePath) === 'supported-models.json') {
 991 | 				return JSON.stringify({
 992 | 					openai: [{ id: 'gpt-4-turbo' }]
 993 | 				});
 994 | 			}
 995 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
 996 | 		});
 997 | 
 998 | 		fsExistsSyncSpy.mockReturnValue(true);
 999 | 
1000 | 		// Ensure getConfig returns new values instead of cached ones
1001 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
1002 | 
1003 | 		// Act
1004 | 		const responseLanguage =
1005 | 			configManager.getResponseLanguage(MOCK_PROJECT_ROOT);
1006 | 
1007 | 		// Assert
1008 | 		expect(responseLanguage).toBe('English');
1009 | 	});
1010 | 
1011 | 	// Add more tests for other getters (getResearchProvider, getProjectName, etc.)
1012 | });
1013 | 
1014 | // --- isConfigFilePresent Tests ---
1015 | describe('isConfigFilePresent', () => {
1016 | 	test('should return true if config file exists', () => {
1017 | 		fsExistsSyncSpy.mockReturnValue(true);
1018 | 		// findProjectRoot mock set in beforeEach
1019 | 		expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(true);
1020 | 		expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
1021 | 	});
1022 | 
1023 | 	test('should return false if config file does not exist', () => {
1024 | 		fsExistsSyncSpy.mockReturnValue(false);
1025 | 		// findProjectRoot mock set in beforeEach
1026 | 		expect(configManager.isConfigFilePresent(MOCK_PROJECT_ROOT)).toBe(false);
1027 | 		expect(fsExistsSyncSpy).toHaveBeenCalledWith(MOCK_CONFIG_PATH);
1028 | 	});
1029 | 
1030 | 	test.skip('should use findProjectRoot if explicitRoot is not provided', () => {
1031 | 		// TODO: Fix mock interaction, findProjectRoot isn't being registered as called
1032 | 		fsExistsSyncSpy.mockReturnValue(true);
1033 | 		// findProjectRoot mock set in beforeEach
1034 | 		expect(configManager.isConfigFilePresent()).toBe(true);
1035 | 		expect(mockFindProjectRoot).toHaveBeenCalled(); // Should be called now
1036 | 	});
1037 | });
1038 | 
1039 | // --- getAllProviders Tests ---
1040 | describe('getAllProviders', () => {
1041 | 	test('should return all providers from ALL_PROVIDERS constant', () => {
1042 | 		// Arrange: Ensure config is loaded with real data
1043 | 		configManager.getConfig(null, true); // Force load using the mock that returns real data
1044 | 
1045 | 		// Act
1046 | 		const providers = configManager.getAllProviders();
1047 | 
1048 | 		// Assert
1049 | 		// getAllProviders() should return the same as the ALL_PROVIDERS constant
1050 | 		expect(providers).toEqual(configManager.ALL_PROVIDERS);
1051 | 		expect(providers.length).toBe(configManager.ALL_PROVIDERS.length);
1052 | 
1053 | 		// Verify it includes both validated and custom providers
1054 | 		expect(providers).toEqual(
1055 | 			expect.arrayContaining(configManager.VALIDATED_PROVIDERS)
1056 | 		);
1057 | 		expect(providers).toEqual(
1058 | 			expect.arrayContaining(Object.values(configManager.CUSTOM_PROVIDERS))
1059 | 		);
1060 | 	});
1061 | });
1062 | 
1063 | // Add tests for getParametersForRole if needed
1064 | 
1065 | // --- defaultNumTasks Tests ---
1066 | describe('Configuration Getters', () => {
1067 | 	test('getDefaultNumTasks should return default value when config is valid', () => {
1068 | 		// Arrange: Mock fs.readFileSync to return valid config when called with the expected path
1069 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
1070 | 			if (filePath === MOCK_CONFIG_PATH) {
1071 | 				return JSON.stringify({
1072 | 					global: {
1073 | 						defaultNumTasks: 15
1074 | 					}
1075 | 				});
1076 | 			}
1077 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
1078 | 		});
1079 | 		fsExistsSyncSpy.mockReturnValue(true);
1080 | 
1081 | 		// Force reload to clear cache
1082 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
1083 | 
1084 | 		// Act: Call getDefaultNumTasks with explicit root
1085 | 		const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT);
1086 | 
1087 | 		// Assert
1088 | 		expect(result).toBe(15);
1089 | 	});
1090 | 
1091 | 	test('getDefaultNumTasks should return fallback when config value is invalid', () => {
1092 | 		// Arrange: Mock fs.readFileSync to return invalid config
1093 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
1094 | 			if (filePath === MOCK_CONFIG_PATH) {
1095 | 				return JSON.stringify({
1096 | 					global: {
1097 | 						defaultNumTasks: 'invalid'
1098 | 					}
1099 | 				});
1100 | 			}
1101 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
1102 | 		});
1103 | 		fsExistsSyncSpy.mockReturnValue(true);
1104 | 
1105 | 		// Force reload to clear cache
1106 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
1107 | 
1108 | 		// Act: Call getDefaultNumTasks with explicit root
1109 | 		const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT);
1110 | 
1111 | 		// Assert
1112 | 		expect(result).toBe(10); // Should fallback to DEFAULTS.global.defaultNumTasks
1113 | 	});
1114 | 
1115 | 	test('getDefaultNumTasks should return fallback when config value is missing', () => {
1116 | 		// Arrange: Mock fs.readFileSync to return config without defaultNumTasks
1117 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
1118 | 			if (filePath === MOCK_CONFIG_PATH) {
1119 | 				return JSON.stringify({
1120 | 					global: {}
1121 | 				});
1122 | 			}
1123 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
1124 | 		});
1125 | 		fsExistsSyncSpy.mockReturnValue(true);
1126 | 
1127 | 		// Force reload to clear cache
1128 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
1129 | 
1130 | 		// Act: Call getDefaultNumTasks with explicit root
1131 | 		const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT);
1132 | 
1133 | 		// Assert
1134 | 		expect(result).toBe(10); // Should fallback to DEFAULTS.global.defaultNumTasks
1135 | 	});
1136 | 
1137 | 	test('getDefaultNumTasks should handle non-existent config file', () => {
1138 | 		// Arrange: Mock file not existing
1139 | 		fsExistsSyncSpy.mockReturnValue(false);
1140 | 
1141 | 		// Force reload to clear cache
1142 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
1143 | 
1144 | 		// Act: Call getDefaultNumTasks with explicit root
1145 | 		const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT);
1146 | 
1147 | 		// Assert
1148 | 		expect(result).toBe(10); // Should fallback to DEFAULTS.global.defaultNumTasks
1149 | 	});
1150 | 
1151 | 	test('getDefaultNumTasks should accept explicit project root', () => {
1152 | 		// Arrange: Mock fs.readFileSync to return valid config
1153 | 		fsReadFileSyncSpy.mockImplementation((filePath) => {
1154 | 			if (filePath === MOCK_CONFIG_PATH) {
1155 | 				return JSON.stringify({
1156 | 					global: {
1157 | 						defaultNumTasks: 20
1158 | 					}
1159 | 				});
1160 | 			}
1161 | 			throw new Error(`Unexpected fs.readFileSync call: ${filePath}`);
1162 | 		});
1163 | 		fsExistsSyncSpy.mockReturnValue(true);
1164 | 
1165 | 		// Force reload to clear cache
1166 | 		configManager.getConfig(MOCK_PROJECT_ROOT, true);
1167 | 
1168 | 		// Act: Call getDefaultNumTasks with explicit project root
1169 | 		const result = configManager.getDefaultNumTasks(MOCK_PROJECT_ROOT);
1170 | 
1171 | 		// Assert
1172 | 		expect(result).toBe(20);
1173 | 	});
1174 | });
1175 | 
1176 | // Note: Tests for setMainModel, setResearchModel were removed as the functions were removed in the implementation.
1177 | // If similar setter functions exist, add tests for them following the writeConfig pattern.
1178 | 
```
Page 55/69FirstPrevNextLast