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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/workflow/services/workflow.service.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview WorkflowService - High-level facade for TDD workflow operations
  3 |  * Provides a simplified API for MCP tools while delegating to WorkflowOrchestrator
  4 |  */
  5 | 
  6 | import { GitAdapter } from '../../git/adapters/git-adapter.js';
  7 | import { WorkflowStateManager } from '../managers/workflow-state-manager.js';
  8 | import { WorkflowOrchestrator } from '../orchestrators/workflow-orchestrator.js';
  9 | import type {
 10 | 	SubtaskInfo,
 11 | 	TDDPhase,
 12 | 	TestResult,
 13 | 	WorkflowContext,
 14 | 	WorkflowPhase,
 15 | 	WorkflowState
 16 | } from '../types.js';
 17 | import { WorkflowActivityLogger } from './workflow-activity-logger.js';
 18 | 
 19 | /**
 20 |  * Options for starting a new workflow
 21 |  */
 22 | export interface StartWorkflowOptions {
 23 | 	taskId: string;
 24 | 	taskTitle: string;
 25 | 	subtasks: Array<{
 26 | 		id: string;
 27 | 		title: string;
 28 | 		status: string;
 29 | 		maxAttempts?: number;
 30 | 	}>;
 31 | 	maxAttempts?: number;
 32 | 	force?: boolean;
 33 | 	tag?: string; // Optional tag for branch naming
 34 | }
 35 | 
 36 | /**
 37 |  * Simplified workflow status for MCP responses
 38 |  */
 39 | export interface WorkflowStatus {
 40 | 	taskId: string;
 41 | 	phase: WorkflowPhase;
 42 | 	tddPhase?: TDDPhase;
 43 | 	branchName?: string;
 44 | 	currentSubtask?: {
 45 | 		id: string;
 46 | 		title: string;
 47 | 		attempts: number;
 48 | 		maxAttempts: number;
 49 | 	};
 50 | 	progress: {
 51 | 		completed: number;
 52 | 		total: number;
 53 | 		current: number;
 54 | 		percentage: number;
 55 | 	};
 56 | }
 57 | 
 58 | /**
 59 |  * Next action recommendation for AI agent
 60 |  */
 61 | export interface NextAction {
 62 | 	action: string;
 63 | 	description: string;
 64 | 	nextSteps: string;
 65 | 	phase: WorkflowPhase;
 66 | 	tddPhase?: TDDPhase;
 67 | 	subtask?: {
 68 | 		id: string;
 69 | 		title: string;
 70 | 	};
 71 | }
 72 | 
 73 | /**
 74 |  * WorkflowService - Facade for workflow operations
 75 |  * Manages WorkflowOrchestrator lifecycle and state persistence
 76 |  */
 77 | export class WorkflowService {
 78 | 	private readonly projectRoot: string;
 79 | 	private readonly stateManager: WorkflowStateManager;
 80 | 	private orchestrator?: WorkflowOrchestrator;
 81 | 	private activityLogger?: WorkflowActivityLogger;
 82 | 
 83 | 	constructor(projectRoot: string) {
 84 | 		this.projectRoot = projectRoot;
 85 | 		this.stateManager = new WorkflowStateManager(projectRoot);
 86 | 	}
 87 | 
 88 | 	/**
 89 | 	 * Check if workflow state exists
 90 | 	 */
 91 | 	async hasWorkflow(): Promise<boolean> {
 92 | 		return await this.stateManager.exists();
 93 | 	}
 94 | 
 95 | 	/**
 96 | 	 * Start a new TDD workflow
 97 | 	 */
 98 | 	async startWorkflow(options: StartWorkflowOptions): Promise<WorkflowStatus> {
 99 | 		const {
100 | 			taskId,
101 | 			taskTitle,
102 | 			subtasks,
103 | 			maxAttempts = 3,
104 | 			force,
105 | 			tag
106 | 		} = options;
107 | 
108 | 		// Check for existing workflow
109 | 		if ((await this.hasWorkflow()) && !force) {
110 | 			throw new Error(
111 | 				'Workflow already exists. Use force=true to override or resume existing workflow.'
112 | 			);
113 | 		}
114 | 
115 | 		// Initialize git adapter and ensure clean state
116 | 		const gitAdapter = new GitAdapter(this.projectRoot);
117 | 		await gitAdapter.ensureGitRepository();
118 | 		await gitAdapter.ensureCleanWorkingTree();
119 | 
120 | 		// Parse subtasks to WorkflowContext format
121 | 		const workflowSubtasks: SubtaskInfo[] = subtasks.map((st) => ({
122 | 			id: st.id,
123 | 			title: st.title,
124 | 			status: st.status === 'done' ? 'completed' : 'pending',
125 | 			attempts: 0,
126 | 			maxAttempts: st.maxAttempts || maxAttempts
127 | 		}));
128 | 
129 | 		// Find the first incomplete subtask to resume from
130 | 		const firstIncompleteIndex = workflowSubtasks.findIndex(
131 | 			(st) => st.status !== 'completed'
132 | 		);
133 | 
134 | 		// If all subtasks are already completed, throw an error
135 | 		if (firstIncompleteIndex === -1) {
136 | 			throw new Error(
137 | 				`All subtasks for task ${taskId} are already completed. Nothing to do.`
138 | 			);
139 | 		}
140 | 
141 | 		// Create workflow context, starting from first incomplete subtask
142 | 		const context: WorkflowContext = {
143 | 			taskId,
144 | 			subtasks: workflowSubtasks,
145 | 			currentSubtaskIndex: firstIncompleteIndex,
146 | 			errors: [],
147 | 			metadata: {
148 | 				startedAt: new Date().toISOString(),
149 | 				taskTitle,
150 | 				resumedFromSubtask:
151 | 					firstIncompleteIndex > 0
152 | 						? workflowSubtasks[firstIncompleteIndex].id
153 | 						: undefined
154 | 			}
155 | 		};
156 | 
157 | 		// Create orchestrator with auto-persistence
158 | 		this.orchestrator = new WorkflowOrchestrator(context);
159 | 		this.orchestrator.enableAutoPersist(async (state: WorkflowState) => {
160 | 			await this.stateManager.save(state);
161 | 		});
162 | 
163 | 		// Initialize activity logger to track all workflow events
164 | 		this.activityLogger = new WorkflowActivityLogger(
165 | 			this.orchestrator,
166 | 			this.stateManager.getActivityLogPath()
167 | 		);
168 | 		this.activityLogger.start();
169 | 
170 | 		// Transition through PREFLIGHT and BRANCH_SETUP phases
171 | 		await this.orchestrator.transition({ type: 'PREFLIGHT_COMPLETE' });
172 | 
173 | 		// Create git branch with descriptive name
174 | 		const branchName = this.generateBranchName(taskId, taskTitle, tag);
175 | 
176 | 		// Check if we're already on the target branch
177 | 		const currentBranch = await gitAdapter.getCurrentBranch();
178 | 		if (currentBranch !== branchName) {
179 | 			// Only create branch if we're not already on it
180 | 			await gitAdapter.createAndCheckoutBranch(branchName);
181 | 		}
182 | 
183 | 		// Transition to SUBTASK_LOOP with RED phase
184 | 		await this.orchestrator.transition({
185 | 			type: 'BRANCH_CREATED',
186 | 			branchName
187 | 		});
188 | 
189 | 		return this.getStatus();
190 | 	}
191 | 
192 | 	/**
193 | 	 * Resume an existing workflow
194 | 	 */
195 | 	async resumeWorkflow(): Promise<WorkflowStatus> {
196 | 		// Load state
197 | 		const state = await this.stateManager.load();
198 | 
199 | 		// Create new orchestrator with loaded context
200 | 		this.orchestrator = new WorkflowOrchestrator(state.context);
201 | 
202 | 		// Validate and restore state
203 | 		if (!this.orchestrator.canResumeFromState(state)) {
204 | 			throw new Error(
205 | 				'Invalid workflow state. State may be corrupted. Consider starting a new workflow.'
206 | 			);
207 | 		}
208 | 
209 | 		this.orchestrator.restoreState(state);
210 | 
211 | 		// Re-enable auto-persistence
212 | 		this.orchestrator.enableAutoPersist(async (newState: WorkflowState) => {
213 | 			await this.stateManager.save(newState);
214 | 		});
215 | 
216 | 		// Initialize activity logger to continue tracking events
217 | 		this.activityLogger = new WorkflowActivityLogger(
218 | 			this.orchestrator,
219 | 			this.stateManager.getActivityLogPath()
220 | 		);
221 | 		this.activityLogger.start();
222 | 
223 | 		return this.getStatus();
224 | 	}
225 | 
226 | 	/**
227 | 	 * Get current workflow status
228 | 	 */
229 | 	getStatus(): WorkflowStatus {
230 | 		if (!this.orchestrator) {
231 | 			throw new Error('No active workflow. Start or resume a workflow first.');
232 | 		}
233 | 
234 | 		const context = this.orchestrator.getContext();
235 | 		const progress = this.orchestrator.getProgress();
236 | 		const currentSubtask = this.orchestrator.getCurrentSubtask();
237 | 
238 | 		return {
239 | 			taskId: context.taskId,
240 | 			phase: this.orchestrator.getCurrentPhase(),
241 | 			tddPhase: this.orchestrator.getCurrentTDDPhase(),
242 | 			branchName: context.branchName,
243 | 			currentSubtask: currentSubtask
244 | 				? {
245 | 						id: currentSubtask.id,
246 | 						title: currentSubtask.title,
247 | 						attempts: currentSubtask.attempts,
248 | 						maxAttempts: currentSubtask.maxAttempts || 3
249 | 					}
250 | 				: undefined,
251 | 			progress
252 | 		};
253 | 	}
254 | 
255 | 	/**
256 | 	 * Get workflow context (for accessing full state details)
257 | 	 */
258 | 	getContext(): WorkflowContext {
259 | 		if (!this.orchestrator) {
260 | 			throw new Error('No active workflow. Start or resume a workflow first.');
261 | 		}
262 | 
263 | 		return this.orchestrator.getContext();
264 | 	}
265 | 
266 | 	/**
267 | 	 * Get next recommended action for AI agent
268 | 	 */
269 | 	getNextAction(): NextAction {
270 | 		if (!this.orchestrator) {
271 | 			throw new Error('No active workflow. Start or resume a workflow first.');
272 | 		}
273 | 
274 | 		const phase = this.orchestrator.getCurrentPhase();
275 | 		const tddPhase = this.orchestrator.getCurrentTDDPhase();
276 | 		const currentSubtask = this.orchestrator.getCurrentSubtask();
277 | 
278 | 		// Determine action based on current phase
279 | 		if (phase === 'COMPLETE') {
280 | 			return {
281 | 				action: 'workflow_complete',
282 | 				description: 'All subtasks completed',
283 | 				nextSteps:
284 | 					'All subtasks completed! Review the entire implementation and merge your branch when ready.',
285 | 				phase
286 | 			};
287 | 		}
288 | 
289 | 		if (phase === 'FINALIZE') {
290 | 			return {
291 | 				action: 'finalize_workflow',
292 | 				description: 'Finalize and complete the workflow',
293 | 				nextSteps:
294 | 					'All subtasks are complete! Use autopilot_finalize to verify no uncommitted changes remain and mark the workflow as complete.',
295 | 				phase
296 | 			};
297 | 		}
298 | 
299 | 		if (phase !== 'SUBTASK_LOOP' || !tddPhase || !currentSubtask) {
300 | 			return {
301 | 				action: 'unknown',
302 | 				description: 'Workflow is not in active state',
303 | 				nextSteps: 'Use autopilot_status to check workflow state.',
304 | 				phase
305 | 			};
306 | 		}
307 | 
308 | 		const baseAction = {
309 | 			phase,
310 | 			tddPhase,
311 | 			subtask: {
312 | 				id: currentSubtask.id,
313 | 				title: currentSubtask.title
314 | 			}
315 | 		};
316 | 
317 | 		switch (tddPhase) {
318 | 			case 'RED':
319 | 				return {
320 | 					...baseAction,
321 | 					action: 'generate_test',
322 | 					description: 'Generate failing test for current subtask',
323 | 					nextSteps: `Write failing tests for subtask ${currentSubtask.id}: "${currentSubtask.title}". Create test file(s) that validate the expected behavior. Run tests and use autopilot_complete_phase with results. Note: If all tests pass (0 failures), the feature is already implemented and the subtask will be auto-completed.`
324 | 				};
325 | 			case 'GREEN':
326 | 				return {
327 | 					...baseAction,
328 | 					action: 'implement_code',
329 | 					description: 'Implement feature to make tests pass',
330 | 					nextSteps: `Implement code to make tests pass for subtask ${currentSubtask.id}: "${currentSubtask.title}". Write the minimal code needed to pass all tests (GREEN phase), then use autopilot_complete_phase with test results.`
331 | 				};
332 | 			case 'COMMIT':
333 | 				return {
334 | 					...baseAction,
335 | 					action: 'commit_changes',
336 | 					description: 'Commit RED-GREEN cycle changes',
337 | 					nextSteps: `Review and commit your changes for subtask ${currentSubtask.id}: "${currentSubtask.title}". Use autopilot_commit to create the commit and advance to the next subtask.`
338 | 				};
339 | 			default:
340 | 				return {
341 | 					...baseAction,
342 | 					action: 'unknown',
343 | 					description: 'Unknown TDD phase',
344 | 					nextSteps: 'Use autopilot_status to check workflow state.'
345 | 				};
346 | 		}
347 | 	}
348 | 
349 | 	/**
350 | 	 * Complete current TDD phase with test results
351 | 	 */
352 | 	async completePhase(testResults: TestResult): Promise<WorkflowStatus> {
353 | 		if (!this.orchestrator) {
354 | 			throw new Error('No active workflow. Start or resume a workflow first.');
355 | 		}
356 | 
357 | 		const tddPhase = this.orchestrator.getCurrentTDDPhase();
358 | 
359 | 		if (!tddPhase) {
360 | 			throw new Error('Not in active TDD phase');
361 | 		}
362 | 
363 | 		// Transition based on current phase
364 | 		switch (tddPhase) {
365 | 			case 'RED':
366 | 				await this.orchestrator.transition({
367 | 					type: 'RED_PHASE_COMPLETE',
368 | 					testResults
369 | 				});
370 | 				break;
371 | 			case 'GREEN':
372 | 				await this.orchestrator.transition({
373 | 					type: 'GREEN_PHASE_COMPLETE',
374 | 					testResults
375 | 				});
376 | 				break;
377 | 			case 'COMMIT':
378 | 				throw new Error(
379 | 					'Cannot complete COMMIT phase with test results. Use commit() instead.'
380 | 				);
381 | 			default:
382 | 				throw new Error(`Unknown TDD phase: ${tddPhase}`);
383 | 		}
384 | 
385 | 		return this.getStatus();
386 | 	}
387 | 
388 | 	/**
389 | 	 * Commit current changes and advance workflow
390 | 	 */
391 | 	async commit(): Promise<WorkflowStatus> {
392 | 		if (!this.orchestrator) {
393 | 			throw new Error('No active workflow. Start or resume a workflow first.');
394 | 		}
395 | 
396 | 		const tddPhase = this.orchestrator.getCurrentTDDPhase();
397 | 
398 | 		if (tddPhase !== 'COMMIT') {
399 | 			throw new Error(
400 | 				`Cannot commit in ${tddPhase} phase. Complete RED and GREEN phases first.`
401 | 			);
402 | 		}
403 | 
404 | 		// Transition COMMIT phase complete
405 | 		await this.orchestrator.transition({
406 | 			type: 'COMMIT_COMPLETE'
407 | 		});
408 | 
409 | 		// Check if should advance to next subtask
410 | 		const progress = this.orchestrator.getProgress();
411 | 		if (progress.current < progress.total) {
412 | 			await this.orchestrator.transition({ type: 'SUBTASK_COMPLETE' });
413 | 		} else {
414 | 			// All subtasks complete
415 | 			await this.orchestrator.transition({ type: 'ALL_SUBTASKS_COMPLETE' });
416 | 		}
417 | 
418 | 		return this.getStatus();
419 | 	}
420 | 
421 | 	/**
422 | 	 * Finalize and complete the workflow
423 | 	 * Validates working tree is clean before marking complete
424 | 	 */
425 | 	async finalizeWorkflow(): Promise<WorkflowStatus> {
426 | 		if (!this.orchestrator) {
427 | 			throw new Error('No active workflow. Start or resume a workflow first.');
428 | 		}
429 | 
430 | 		const phase = this.orchestrator.getCurrentPhase();
431 | 		if (phase !== 'FINALIZE') {
432 | 			throw new Error(
433 | 				`Cannot finalize workflow in ${phase} phase. Complete all subtasks first.`
434 | 			);
435 | 		}
436 | 
437 | 		// Check working tree is clean
438 | 		const gitAdapter = new GitAdapter(this.projectRoot);
439 | 		const statusSummary = await gitAdapter.getStatusSummary();
440 | 
441 | 		if (!statusSummary.isClean) {
442 | 			throw new Error(
443 | 				`Cannot finalize workflow: working tree has uncommitted changes.\n` +
444 | 					`Staged: ${statusSummary.staged}, Modified: ${statusSummary.modified}, ` +
445 | 					`Deleted: ${statusSummary.deleted}, Untracked: ${statusSummary.untracked}\n` +
446 | 					`Please commit all changes before finalizing the workflow.`
447 | 			);
448 | 		}
449 | 
450 | 		// Transition to COMPLETE
451 | 		await this.orchestrator.transition({ type: 'FINALIZE_COMPLETE' });
452 | 
453 | 		return this.getStatus();
454 | 	}
455 | 
456 | 	/**
457 | 	 * Abort current workflow
458 | 	 */
459 | 	async abortWorkflow(): Promise<void> {
460 | 		if (this.orchestrator) {
461 | 			await this.orchestrator.transition({ type: 'ABORT' });
462 | 		}
463 | 
464 | 		// Delete state file
465 | 		await this.stateManager.delete();
466 | 
467 | 		this.orchestrator = undefined;
468 | 	}
469 | 
470 | 	/**
471 | 	 * Generate a descriptive git branch name
472 | 	 * Format: tag-name/task-id-task-title or task-id-task-title
473 | 	 */
474 | 	private generateBranchName(
475 | 		taskId: string,
476 | 		taskTitle: string,
477 | 		tag?: string
478 | 	): string {
479 | 		// Sanitize task title for branch name
480 | 		const sanitizedTitle = taskTitle
481 | 			.toLowerCase()
482 | 			.replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with dash
483 | 			.replace(/^-+|-+$/g, '') // Remove leading/trailing dashes
484 | 			.substring(0, 50); // Limit length
485 | 
486 | 		// Format task ID for branch name
487 | 		const formattedTaskId = taskId.replace(/\./g, '-');
488 | 
489 | 		// Add tag prefix if tag is provided
490 | 		const tagPrefix = tag ? `${tag}/` : '';
491 | 
492 | 		return `${tagPrefix}task-${formattedTaskId}-${sanitizedTitle}`;
493 | 	}
494 | }
495 | 
```

--------------------------------------------------------------------------------
/packages/ai-sdk-provider-grok-cli/src/grok-cli-language-model.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Grok CLI Language Model implementation for AI SDK v5
  3 |  */
  4 | 
  5 | import { spawn } from 'child_process';
  6 | import { promises as fs } from 'fs';
  7 | import { homedir } from 'os';
  8 | import { join } from 'path';
  9 | import type {
 10 | 	LanguageModelV2,
 11 | 	LanguageModelV2CallOptions,
 12 | 	LanguageModelV2CallWarning
 13 | } from '@ai-sdk/provider';
 14 | import { NoSuchModelError } from '@ai-sdk/provider';
 15 | import { generateId } from '@ai-sdk/provider-utils';
 16 | 
 17 | import {
 18 | 	createAPICallError,
 19 | 	createAuthenticationError,
 20 | 	createInstallationError,
 21 | 	createTimeoutError
 22 | } from './errors.js';
 23 | import { extractJson } from './json-extractor.js';
 24 | import {
 25 | 	convertFromGrokCliResponse,
 26 | 	createPromptFromMessages,
 27 | 	escapeShellArg
 28 | } from './message-converter.js';
 29 | import type {
 30 | 	GrokCliLanguageModelOptions,
 31 | 	GrokCliModelId,
 32 | 	GrokCliSettings
 33 | } from './types.js';
 34 | 
 35 | /**
 36 |  * Grok CLI Language Model implementation for AI SDK v5
 37 |  */
 38 | export class GrokCliLanguageModel implements LanguageModelV2 {
 39 | 	readonly specificationVersion = 'v2' as const;
 40 | 	readonly defaultObjectGenerationMode = 'json' as const;
 41 | 	readonly supportsImageUrls = false;
 42 | 	readonly supportsStructuredOutputs = false;
 43 | 	readonly supportedUrls: Record<string, RegExp[]> = {};
 44 | 
 45 | 	readonly modelId: GrokCliModelId;
 46 | 	readonly settings: GrokCliSettings;
 47 | 
 48 | 	constructor(options: GrokCliLanguageModelOptions) {
 49 | 		this.modelId = options.id;
 50 | 		this.settings = options.settings ?? {};
 51 | 
 52 | 		// Validate model ID format
 53 | 		if (
 54 | 			!this.modelId ||
 55 | 			typeof this.modelId !== 'string' ||
 56 | 			this.modelId.trim() === ''
 57 | 		) {
 58 | 			throw new NoSuchModelError({
 59 | 				modelId: this.modelId,
 60 | 				modelType: 'languageModel'
 61 | 			});
 62 | 		}
 63 | 	}
 64 | 
 65 | 	get provider(): string {
 66 | 		return 'grok-cli';
 67 | 	}
 68 | 
 69 | 	/**
 70 | 	 * Check if Grok CLI is installed and available
 71 | 	 */
 72 | 	private async checkGrokCliInstallation(): Promise<boolean> {
 73 | 		return new Promise((resolve) => {
 74 | 			const child = spawn('grok', ['--version'], {
 75 | 				stdio: 'pipe'
 76 | 			});
 77 | 
 78 | 			child.on('error', () => resolve(false));
 79 | 			child.on('exit', (code) => resolve(code === 0));
 80 | 		});
 81 | 	}
 82 | 
 83 | 	/**
 84 | 	 * Get API key from settings or environment
 85 | 	 */
 86 | 	private async getApiKey(): Promise<string | null> {
 87 | 		// Check settings first
 88 | 		if (this.settings.apiKey) {
 89 | 			return this.settings.apiKey;
 90 | 		}
 91 | 
 92 | 		// Check environment variable
 93 | 		if (process.env.GROK_CLI_API_KEY) {
 94 | 			return process.env.GROK_CLI_API_KEY;
 95 | 		}
 96 | 
 97 | 		// Check grok-cli config file
 98 | 		try {
 99 | 			const configPath = join(homedir(), '.grok', 'user-settings.json');
100 | 			const configContent = await fs.readFile(configPath, 'utf8');
101 | 			const config = JSON.parse(configContent);
102 | 			return config.apiKey || null;
103 | 		} catch (error) {
104 | 			return null;
105 | 		}
106 | 	}
107 | 
108 | 	/**
109 | 	 * Execute Grok CLI command
110 | 	 */
111 | 	private async executeGrokCli(
112 | 		args: string[],
113 | 		options: { timeout?: number; apiKey?: string } = {}
114 | 	): Promise<{ stdout: string; stderr: string; exitCode: number }> {
115 | 		// Default timeout based on model type
116 | 		let defaultTimeout = 120000; // 2 minutes default
117 | 		if (this.modelId.includes('grok-4')) {
118 | 			defaultTimeout = 600000; // 10 minutes for grok-4 models (they seem to hang during setup)
119 | 		}
120 | 
121 | 		const timeout = options.timeout ?? this.settings.timeout ?? defaultTimeout;
122 | 
123 | 		return new Promise((resolve, reject) => {
124 | 			const child = spawn('grok', args, {
125 | 				stdio: 'pipe',
126 | 				cwd: this.settings.workingDirectory || process.cwd(),
127 | 				env:
128 | 					options.apiKey === undefined
129 | 						? process.env
130 | 						: { ...process.env, GROK_CLI_API_KEY: options.apiKey }
131 | 			});
132 | 
133 | 			let stdout = '';
134 | 			let stderr = '';
135 | 			let timeoutId: NodeJS.Timeout | undefined;
136 | 
137 | 			// Set up timeout
138 | 			if (timeout > 0) {
139 | 				timeoutId = setTimeout(() => {
140 | 					child.kill('SIGTERM');
141 | 					reject(
142 | 						createTimeoutError({
143 | 							message: `Grok CLI command timed out after ${timeout}ms`,
144 | 							timeoutMs: timeout,
145 | 							promptExcerpt: args.join(' ').substring(0, 200)
146 | 						})
147 | 					);
148 | 				}, timeout);
149 | 			}
150 | 
151 | 			child.stdout?.on('data', (data) => {
152 | 				const chunk = data.toString();
153 | 				stdout += chunk;
154 | 			});
155 | 
156 | 			child.stderr?.on('data', (data) => {
157 | 				const chunk = data.toString();
158 | 				stderr += chunk;
159 | 			});
160 | 
161 | 			child.on('error', (error) => {
162 | 				if (timeoutId) clearTimeout(timeoutId);
163 | 
164 | 				if ((error as any).code === 'ENOENT') {
165 | 					reject(createInstallationError({}));
166 | 				} else {
167 | 					reject(
168 | 						createAPICallError({
169 | 							message: `Failed to execute Grok CLI: ${error.message}`,
170 | 							code: (error as any).code,
171 | 							stderr: error.message,
172 | 							isRetryable: false
173 | 						})
174 | 					);
175 | 				}
176 | 			});
177 | 
178 | 			child.on('exit', (exitCode) => {
179 | 				if (timeoutId) clearTimeout(timeoutId);
180 | 
181 | 				resolve({
182 | 					stdout: stdout.trim(),
183 | 					stderr: stderr.trim(),
184 | 					exitCode: exitCode || 0
185 | 				});
186 | 			});
187 | 		});
188 | 	}
189 | 
190 | 	/**
191 | 	 * Generate comprehensive warnings for unsupported parameters and validation issues
192 | 	 */
193 | 	private generateAllWarnings(
194 | 		options: LanguageModelV2CallOptions,
195 | 		prompt: string
196 | 	): LanguageModelV2CallWarning[] {
197 | 		const warnings: LanguageModelV2CallWarning[] = [];
198 | 		const unsupportedParams: string[] = [];
199 | 
200 | 		// Check for unsupported parameters
201 | 		if (options.temperature !== undefined)
202 | 			unsupportedParams.push('temperature');
203 | 		if (options.topP !== undefined) unsupportedParams.push('topP');
204 | 		if (options.topK !== undefined) unsupportedParams.push('topK');
205 | 		if (options.presencePenalty !== undefined)
206 | 			unsupportedParams.push('presencePenalty');
207 | 		if (options.frequencyPenalty !== undefined)
208 | 			unsupportedParams.push('frequencyPenalty');
209 | 		if (options.stopSequences !== undefined && options.stopSequences.length > 0)
210 | 			unsupportedParams.push('stopSequences');
211 | 		if (options.seed !== undefined) unsupportedParams.push('seed');
212 | 
213 | 		if (unsupportedParams.length > 0) {
214 | 			// Add a warning for each unsupported parameter
215 | 			for (const param of unsupportedParams) {
216 | 				warnings.push({
217 | 					type: 'unsupported-setting',
218 | 					setting: param as
219 | 						| 'temperature'
220 | 						| 'topP'
221 | 						| 'topK'
222 | 						| 'presencePenalty'
223 | 						| 'frequencyPenalty'
224 | 						| 'stopSequences'
225 | 						| 'seed',
226 | 					details: `Grok CLI does not support the ${param} parameter. It will be ignored.`
227 | 				});
228 | 			}
229 | 		}
230 | 
231 | 		// Add model validation warnings if needed
232 | 		if (!this.modelId || this.modelId.trim() === '') {
233 | 			warnings.push({
234 | 				type: 'other',
235 | 				message: 'Model ID is empty or invalid'
236 | 			});
237 | 		}
238 | 
239 | 		// Add prompt validation
240 | 		if (!prompt || prompt.trim() === '') {
241 | 			warnings.push({
242 | 				type: 'other',
243 | 				message: 'Prompt is empty'
244 | 			});
245 | 		}
246 | 
247 | 		return warnings;
248 | 	}
249 | 
250 | 	/**
251 | 	 * Generate text using Grok CLI
252 | 	 */
253 | 	async doGenerate(options: LanguageModelV2CallOptions) {
254 | 		// Handle abort signal early
255 | 		if (options.abortSignal?.aborted) {
256 | 			throw options.abortSignal.reason || new Error('Request aborted');
257 | 		}
258 | 
259 | 		// Check CLI installation
260 | 		const isInstalled = await this.checkGrokCliInstallation();
261 | 		if (!isInstalled) {
262 | 			throw createInstallationError({});
263 | 		}
264 | 
265 | 		// Get API key
266 | 		const apiKey = await this.getApiKey();
267 | 		if (!apiKey) {
268 | 			throw createAuthenticationError({
269 | 				message:
270 | 					'Grok CLI API key not found. Set GROK_CLI_API_KEY environment variable or configure grok-cli.'
271 | 			});
272 | 		}
273 | 
274 | 		const prompt = createPromptFromMessages(options.prompt);
275 | 		const warnings = this.generateAllWarnings(options, prompt);
276 | 
277 | 		// Build command arguments
278 | 		const args = ['--prompt', escapeShellArg(prompt)];
279 | 
280 | 		// Add model if specified
281 | 		if (this.modelId && this.modelId !== 'default') {
282 | 			args.push('--model', this.modelId);
283 | 		}
284 | 
285 | 		// Skip API key parameter if it's likely already configured to avoid hanging
286 | 		// The CLI seems to hang when trying to save API keys for grok-4 models
287 | 		// if (apiKey) {
288 | 		//	args.push('--api-key', apiKey);
289 | 		// }
290 | 
291 | 		// Add base URL if provided in settings
292 | 		if (this.settings.baseURL) {
293 | 			args.push('--base-url', this.settings.baseURL);
294 | 		}
295 | 
296 | 		// Add working directory if specified
297 | 		if (this.settings.workingDirectory) {
298 | 			args.push('--directory', this.settings.workingDirectory);
299 | 		}
300 | 
301 | 		try {
302 | 			const result = await this.executeGrokCli(args, { apiKey });
303 | 
304 | 			if (result.exitCode !== 0) {
305 | 				// Handle authentication errors
306 | 				if (
307 | 					result.stderr.toLowerCase().includes('unauthorized') ||
308 | 					result.stderr.toLowerCase().includes('authentication')
309 | 				) {
310 | 					throw createAuthenticationError({
311 | 						message: `Grok CLI authentication failed: ${result.stderr}`
312 | 					});
313 | 				}
314 | 
315 | 				throw createAPICallError({
316 | 					message: `Grok CLI failed with exit code ${result.exitCode}: ${result.stderr || 'Unknown error'}`,
317 | 					exitCode: result.exitCode,
318 | 					stderr: result.stderr,
319 | 					stdout: result.stdout,
320 | 					promptExcerpt: prompt.substring(0, 200),
321 | 					isRetryable: false
322 | 				});
323 | 			}
324 | 
325 | 			// Parse response
326 | 			const response = convertFromGrokCliResponse(result.stdout);
327 | 			let text = response.text || '';
328 | 
329 | 			// Extract JSON if in object-json mode
330 | 			const isObjectJson = (
331 | 				o: unknown
332 | 			): o is { mode: { type: 'object-json' } } =>
333 | 				!!o &&
334 | 				typeof o === 'object' &&
335 | 				'mode' in o &&
336 | 				(o as any).mode?.type === 'object-json';
337 | 			if (isObjectJson(options) && text) {
338 | 				text = extractJson(text);
339 | 			}
340 | 
341 | 			return {
342 | 				content: [
343 | 					{
344 | 						type: 'text' as const,
345 | 						text: text || ''
346 | 					}
347 | 				],
348 | 				usage: response.usage
349 | 					? {
350 | 							inputTokens: response.usage.promptTokens,
351 | 							outputTokens: response.usage.completionTokens,
352 | 							totalTokens: response.usage.totalTokens
353 | 						}
354 | 					: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
355 | 				finishReason: 'stop' as const,
356 | 				rawCall: {
357 | 					rawPrompt: prompt,
358 | 					rawSettings: args
359 | 				},
360 | 				warnings: warnings,
361 | 				response: {
362 | 					id: generateId(),
363 | 					timestamp: new Date(),
364 | 					modelId: this.modelId
365 | 				},
366 | 				request: {
367 | 					body: prompt
368 | 				},
369 | 				providerMetadata: {
370 | 					'grok-cli': {
371 | 						exitCode: result.exitCode,
372 | 						...(result.stderr && { stderr: result.stderr })
373 | 					}
374 | 				}
375 | 			};
376 | 		} catch (error) {
377 | 			// Re-throw our custom errors
378 | 			if (
379 | 				(error as any).name === 'APICallError' ||
380 | 				(error as any).name === 'LoadAPIKeyError'
381 | 			) {
382 | 				throw error;
383 | 			}
384 | 
385 | 			// Wrap other errors
386 | 			throw createAPICallError({
387 | 				message: `Grok CLI execution failed: ${(error as Error).message}`,
388 | 				code: (error as any).code,
389 | 				promptExcerpt: prompt.substring(0, 200),
390 | 				isRetryable: false
391 | 			});
392 | 		}
393 | 	}
394 | 
395 | 	/**
396 | 	 * Stream text using Grok CLI
397 | 	 * Note: Grok CLI doesn't natively support streaming, so this simulates streaming
398 | 	 * by generating the full response and then streaming it in chunks
399 | 	 */
400 | 	async doStream(options: LanguageModelV2CallOptions) {
401 | 		const prompt = createPromptFromMessages(options.prompt);
402 | 		const warnings = this.generateAllWarnings(options, prompt);
403 | 
404 | 		const stream = new ReadableStream({
405 | 			start: async (controller) => {
406 | 				let abortListener: (() => void) | undefined;
407 | 
408 | 				try {
409 | 					// Handle abort signal
410 | 					if (options.abortSignal?.aborted) {
411 | 						throw options.abortSignal.reason || new Error('Request aborted');
412 | 					}
413 | 
414 | 					// Set up abort listener
415 | 					if (options.abortSignal) {
416 | 						abortListener = () => {
417 | 							controller.enqueue({
418 | 								type: 'error',
419 | 								error:
420 | 									options.abortSignal?.reason || new Error('Request aborted')
421 | 							});
422 | 							controller.close();
423 | 						};
424 | 						options.abortSignal.addEventListener('abort', abortListener, {
425 | 							once: true
426 | 						});
427 | 					}
428 | 
429 | 					// Emit stream-start with warnings
430 | 					controller.enqueue({ type: 'stream-start', warnings });
431 | 
432 | 					// Generate the full response first
433 | 					const result = await this.doGenerate(options);
434 | 
435 | 					// Emit response metadata
436 | 					controller.enqueue({
437 | 						type: 'response-metadata',
438 | 						id: result.response.id,
439 | 						timestamp: result.response.timestamp,
440 | 						modelId: result.response.modelId
441 | 					});
442 | 
443 | 					// Simulate streaming by chunking the text
444 | 					const content = result.content || [];
445 | 					const text =
446 | 						content.length > 0 && content[0].type === 'text'
447 | 							? content[0].text
448 | 							: '';
449 | 					const chunkSize = 50; // Characters per chunk
450 | 					let textPartId: string | undefined;
451 | 
452 | 					// Emit text-start if we have content
453 | 					if (text.length > 0) {
454 | 						textPartId = generateId();
455 | 						controller.enqueue({
456 | 							type: 'text-start',
457 | 							id: textPartId
458 | 						});
459 | 					}
460 | 
461 | 					for (let i = 0; i < text.length; i += chunkSize) {
462 | 						// Check for abort during streaming
463 | 						if (options.abortSignal?.aborted) {
464 | 							throw options.abortSignal.reason || new Error('Request aborted');
465 | 						}
466 | 
467 | 						const chunk = text.slice(i, i + chunkSize);
468 | 						controller.enqueue({
469 | 							type: 'text-delta',
470 | 							id: textPartId!,
471 | 							delta: chunk
472 | 						});
473 | 
474 | 						// Add small delay to simulate streaming
475 | 						await new Promise((resolve) => setTimeout(resolve, 20));
476 | 					}
477 | 
478 | 					// Close text part if opened
479 | 					if (textPartId) {
480 | 						controller.enqueue({
481 | 							type: 'text-end',
482 | 							id: textPartId
483 | 						});
484 | 					}
485 | 
486 | 					// Emit finish event
487 | 					controller.enqueue({
488 | 						type: 'finish',
489 | 						finishReason: result.finishReason,
490 | 						usage: result.usage,
491 | 						providerMetadata: result.providerMetadata
492 | 					});
493 | 
494 | 					controller.close();
495 | 				} catch (error) {
496 | 					controller.enqueue({
497 | 						type: 'error',
498 | 						error
499 | 					});
500 | 					controller.close();
501 | 				} finally {
502 | 					// Clean up abort listener
503 | 					if (options.abortSignal && abortListener) {
504 | 						options.abortSignal.removeEventListener('abort', abortListener);
505 | 					}
506 | 				}
507 | 			},
508 | 			cancel: () => {
509 | 				// Clean up if stream is cancelled
510 | 			}
511 | 		});
512 | 
513 | 		return {
514 | 			stream,
515 | 			request: {
516 | 				body: prompt
517 | 			}
518 | 		};
519 | 	}
520 | }
521 | 
```

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

```javascript
  1 | /**
  2 |  * Tests for the analyze-task-complexity.js module
  3 |  */
  4 | import { jest } from '@jest/globals';
  5 | import {
  6 | 	createGetTagAwareFilePathMock,
  7 | 	createSlugifyTagForFilePathMock
  8 | } from './setup.js';
  9 | 
 10 | // Mock the dependencies before importing the module under test
 11 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
 12 | 	readJSON: jest.fn(),
 13 | 	writeJSON: jest.fn(),
 14 | 	log: jest.fn(),
 15 | 	CONFIG: {
 16 | 		model: 'mock-claude-model',
 17 | 		maxTokens: 4000,
 18 | 		temperature: 0.7,
 19 | 		debug: false,
 20 | 		defaultSubtasks: 3
 21 | 	},
 22 | 	findTaskById: jest.fn(),
 23 | 	readComplexityReport: jest.fn(),
 24 | 	findTaskInComplexityReport: jest.fn(),
 25 | 	findProjectRoot: jest.fn(() => '/mock/project/root'),
 26 | 	resolveEnvVariable: jest.fn((varName) => `mock_${varName}`),
 27 | 	isSilentMode: jest.fn(() => false),
 28 | 	findCycles: jest.fn(() => []),
 29 | 	formatTaskId: jest.fn((id) => `Task ${id}`),
 30 | 	taskExists: jest.fn((tasks, id) => tasks.some((t) => t.id === id)),
 31 | 	enableSilentMode: jest.fn(),
 32 | 	disableSilentMode: jest.fn(),
 33 | 	truncate: jest.fn((text) => text),
 34 | 	addComplexityToTask: jest.fn((task, complexity) => ({ ...task, complexity })),
 35 | 	aggregateTelemetry: jest.fn((telemetryArray) => telemetryArray[0] || {}),
 36 | 	ensureTagMetadata: jest.fn((tagObj) => tagObj),
 37 | 	getCurrentTag: jest.fn(() => 'master'),
 38 | 	resolveTag: jest.fn(() => 'master'),
 39 | 	flattenTasksWithSubtasks: jest.fn((tasks) => tasks),
 40 | 	getTagAwareFilePath: createGetTagAwareFilePathMock(),
 41 | 	slugifyTagForFilePath: createSlugifyTagForFilePathMock(),
 42 | 	markMigrationForNotice: jest.fn(),
 43 | 	performCompleteTagMigration: jest.fn(),
 44 | 	setTasksForTag: jest.fn(),
 45 | 	getTasksForTag: jest.fn((data, tag) => data[tag]?.tasks || []),
 46 | 	traverseDependencies: jest.fn((tasks, taskId, visited) => [])
 47 | }));
 48 | 
 49 | jest.unstable_mockModule(
 50 | 	'../../../../../scripts/modules/ai-services-unified.js',
 51 | 	() => ({
 52 | 		generateObjectService: jest.fn().mockResolvedValue({
 53 | 			mainResult: {
 54 | 				complexityAnalysis: []
 55 | 			},
 56 | 			telemetryData: {
 57 | 				timestamp: new Date().toISOString(),
 58 | 				userId: '1234567890',
 59 | 				commandName: 'analyze-complexity',
 60 | 				modelUsed: 'claude-3-5-sonnet',
 61 | 				providerName: 'anthropic',
 62 | 				inputTokens: 1000,
 63 | 				outputTokens: 500,
 64 | 				totalTokens: 1500,
 65 | 				totalCost: 0.012414,
 66 | 				currency: 'USD'
 67 | 			}
 68 | 		}),
 69 | 		generateTextService: jest.fn().mockResolvedValue({
 70 | 			mainResult: '[]',
 71 | 			telemetryData: {
 72 | 				timestamp: new Date().toISOString(),
 73 | 				userId: '1234567890',
 74 | 				commandName: 'analyze-complexity',
 75 | 				modelUsed: 'claude-3-5-sonnet',
 76 | 				providerName: 'anthropic',
 77 | 				inputTokens: 1000,
 78 | 				outputTokens: 500,
 79 | 				totalTokens: 1500,
 80 | 				totalCost: 0.012414,
 81 | 				currency: 'USD'
 82 | 			}
 83 | 		}),
 84 | 		streamTextService: jest.fn().mockResolvedValue({
 85 | 			mainResult: async function* () {
 86 | 				yield '{"tasks":[';
 87 | 				yield '{"id":1,"title":"Test Task","priority":"high"}';
 88 | 				yield ']}';
 89 | 			},
 90 | 			telemetryData: {
 91 | 				timestamp: new Date().toISOString(),
 92 | 				userId: '1234567890',
 93 | 				commandName: 'analyze-complexity',
 94 | 				modelUsed: 'claude-3-5-sonnet',
 95 | 				providerName: 'anthropic',
 96 | 				inputTokens: 1000,
 97 | 				outputTokens: 500,
 98 | 				totalTokens: 1500,
 99 | 				totalCost: 0.012414,
100 | 				currency: 'USD'
101 | 			}
102 | 		}),
103 | 		streamObjectService: jest.fn().mockImplementation(async () => {
104 | 			return {
105 | 				get partialObjectStream() {
106 | 					return (async function* () {
107 | 						yield { tasks: [] };
108 | 						yield { tasks: [{ id: 1, title: 'Test Task', priority: 'high' }] };
109 | 					})();
110 | 				},
111 | 				object: Promise.resolve({
112 | 					tasks: [{ id: 1, title: 'Test Task', priority: 'high' }]
113 | 				})
114 | 			};
115 | 		})
116 | 	})
117 | );
118 | 
119 | jest.unstable_mockModule(
120 | 	'../../../../../scripts/modules/config-manager.js',
121 | 	() => ({
122 | 		// Core config access
123 | 		getConfig: jest.fn(() => ({
124 | 			models: { main: { provider: 'anthropic', modelId: 'claude-3-5-sonnet' } },
125 | 			global: { projectName: 'Test Project' }
126 | 		})),
127 | 		writeConfig: jest.fn(() => true),
128 | 		ConfigurationError: class extends Error {},
129 | 		isConfigFilePresent: jest.fn(() => true),
130 | 
131 | 		// Validation
132 | 		validateProvider: jest.fn(() => true),
133 | 		validateProviderModelCombination: jest.fn(() => true),
134 | 		VALID_PROVIDERS: ['anthropic', 'openai', 'perplexity'],
135 | 		MODEL_MAP: {
136 | 			anthropic: [
137 | 				{
138 | 					id: 'claude-3-5-sonnet',
139 | 					cost_per_1m_tokens: { input: 3, output: 15 }
140 | 				}
141 | 			],
142 | 			openai: [{ id: 'gpt-4', cost_per_1m_tokens: { input: 30, output: 60 } }]
143 | 		},
144 | 		getAvailableModels: jest.fn(() => [
145 | 			{
146 | 				id: 'claude-3-5-sonnet',
147 | 				name: 'Claude 3.5 Sonnet',
148 | 				provider: 'anthropic'
149 | 			},
150 | 			{ id: 'gpt-4', name: 'GPT-4', provider: 'openai' }
151 | 		]),
152 | 
153 | 		// Role-specific getters
154 | 		getMainProvider: jest.fn(() => 'anthropic'),
155 | 		getMainModelId: jest.fn(() => 'claude-3-5-sonnet'),
156 | 		getMainMaxTokens: jest.fn(() => 4000),
157 | 		getMainTemperature: jest.fn(() => 0.7),
158 | 		getResearchProvider: jest.fn(() => 'perplexity'),
159 | 		getResearchModelId: jest.fn(() => 'sonar-pro'),
160 | 		getResearchMaxTokens: jest.fn(() => 8700),
161 | 		getResearchTemperature: jest.fn(() => 0.1),
162 | 		getFallbackProvider: jest.fn(() => 'anthropic'),
163 | 		getFallbackModelId: jest.fn(() => 'claude-3-5-sonnet'),
164 | 		getFallbackMaxTokens: jest.fn(() => 4000),
165 | 		getFallbackTemperature: jest.fn(() => 0.7),
166 | 		getBaseUrlForRole: jest.fn(() => undefined),
167 | 
168 | 		// Global setting getters
169 | 		getLogLevel: jest.fn(() => 'info'),
170 | 		getDebugFlag: jest.fn(() => false),
171 | 		getDefaultNumTasks: jest.fn(() => 10),
172 | 		getDefaultSubtasks: jest.fn(() => 5),
173 | 		getDefaultPriority: jest.fn(() => 'medium'),
174 | 		getProjectName: jest.fn(() => 'Test Project'),
175 | 		getOllamaBaseURL: jest.fn(() => 'http://localhost:11434/api'),
176 | 		getAzureBaseURL: jest.fn(() => undefined),
177 | 		getBedrockBaseURL: jest.fn(() => undefined),
178 | 		getParametersForRole: jest.fn(() => ({
179 | 			maxTokens: 4000,
180 | 			temperature: 0.7
181 | 		})),
182 | 		getUserId: jest.fn(() => '1234567890'),
183 | 
184 | 		// API Key Checkers
185 | 		isApiKeySet: jest.fn(() => true),
186 | 		getMcpApiKeyStatus: jest.fn(() => true),
187 | 
188 | 		// Additional functions
189 | 		getAllProviders: jest.fn(() => ['anthropic', 'openai', 'perplexity']),
190 | 		getVertexProjectId: jest.fn(() => undefined),
191 | 		getVertexLocation: jest.fn(() => undefined),
192 | 		hasCodebaseAnalysis: jest.fn(() => false)
193 | 	})
194 | );
195 | 
196 | // Mock fs module
197 | const mockWriteFileSync = jest.fn();
198 | jest.unstable_mockModule('fs', () => ({
199 | 	default: {
200 | 		existsSync: jest.fn(() => false),
201 | 		readFileSync: jest.fn(),
202 | 		writeFileSync: mockWriteFileSync,
203 | 		unlinkSync: jest.fn()
204 | 	},
205 | 	existsSync: jest.fn(() => false),
206 | 	readFileSync: jest.fn(),
207 | 	writeFileSync: mockWriteFileSync,
208 | 	unlinkSync: jest.fn()
209 | }));
210 | 
211 | jest.unstable_mockModule(
212 | 	'../../../../../scripts/modules/prompt-manager.js',
213 | 	() => ({
214 | 		getPromptManager: jest.fn().mockReturnValue({
215 | 			loadPrompt: jest.fn().mockResolvedValue({
216 | 				systemPrompt: 'Mocked system prompt',
217 | 				userPrompt: 'Mocked user prompt'
218 | 			})
219 | 		})
220 | 	})
221 | );
222 | 
223 | // Import the mocked modules
224 | const { readJSON, writeJSON, log, CONFIG, findTaskById } = await import(
225 | 	'../../../../../scripts/modules/utils.js'
226 | );
227 | 
228 | const { generateObjectService, generateTextService, streamTextService } =
229 | 	await import('../../../../../scripts/modules/ai-services-unified.js');
230 | 
231 | const fs = await import('fs');
232 | 
233 | // Import the module under test
234 | const { default: analyzeTaskComplexity } = await import(
235 | 	'../../../../../scripts/modules/task-manager/analyze-task-complexity.js'
236 | );
237 | 
238 | describe('analyzeTaskComplexity', () => {
239 | 	// Sample response structure (simplified for these tests)
240 | 	const sampleApiResponse = {
241 | 		mainResult: JSON.stringify({
242 | 			tasks: [
243 | 				{ id: 1, complexity: 3, subtaskCount: 2 },
244 | 				{ id: 2, complexity: 7, subtaskCount: 5 },
245 | 				{ id: 3, complexity: 9, subtaskCount: 8 }
246 | 			]
247 | 		}),
248 | 		telemetryData: {
249 | 			timestamp: new Date().toISOString(),
250 | 			userId: '1234567890',
251 | 			commandName: 'analyze-complexity',
252 | 			modelUsed: 'claude-3-5-sonnet',
253 | 			providerName: 'anthropic',
254 | 			inputTokens: 1000,
255 | 			outputTokens: 500,
256 | 			totalTokens: 1500,
257 | 			totalCost: 0.012414,
258 | 			currency: 'USD'
259 | 		}
260 | 	};
261 | 
262 | 	const sampleTasks = {
263 | 		master: {
264 | 			tasks: [
265 | 				{
266 | 					id: 1,
267 | 					title: 'Task 1',
268 | 					description: 'First task description',
269 | 					status: 'pending',
270 | 					dependencies: [],
271 | 					priority: 'high'
272 | 				},
273 | 				{
274 | 					id: 2,
275 | 					title: 'Task 2',
276 | 					description: 'Second task description',
277 | 					status: 'pending',
278 | 					dependencies: [1],
279 | 					priority: 'medium'
280 | 				},
281 | 				{
282 | 					id: 3,
283 | 					title: 'Task 3',
284 | 					description: 'Third task description',
285 | 					status: 'done',
286 | 					dependencies: [1, 2],
287 | 					priority: 'high'
288 | 				}
289 | 			]
290 | 		}
291 | 	};
292 | 
293 | 	beforeEach(() => {
294 | 		jest.clearAllMocks();
295 | 
296 | 		// Default mock implementations - readJSON should return the resolved view with tasks at top level
297 | 		readJSON.mockImplementation((tasksPath, projectRoot, tag) => {
298 | 			return {
299 | 				...sampleTasks.master,
300 | 				tag: tag || 'master',
301 | 				_rawTaggedData: sampleTasks
302 | 			};
303 | 		});
304 | 
305 | 		// Mock findTaskById to return the expected structure
306 | 		findTaskById.mockImplementation((tasks, taskId) => {
307 | 			const task = tasks?.find((t) => t.id === parseInt(taskId));
308 | 			return { task: task || null, originalSubtaskCount: null };
309 | 		});
310 | 
311 | 		generateObjectService.mockResolvedValue({
312 | 			mainResult: {
313 | 				complexityAnalysis: JSON.parse(sampleApiResponse.mainResult).tasks
314 | 			},
315 | 			telemetryData: sampleApiResponse.telemetryData
316 | 		});
317 | 	});
318 | 
319 | 	test('should call generateObjectService with the correct parameters', async () => {
320 | 		// Arrange
321 | 		const options = {
322 | 			file: 'tasks/tasks.json',
323 | 			output: 'scripts/task-complexity-report.json',
324 | 			threshold: '5',
325 | 			research: false,
326 | 			projectRoot: '/mock/project/root'
327 | 		};
328 | 
329 | 		// Act
330 | 		await analyzeTaskComplexity(options, {
331 | 			projectRoot: '/mock/project/root',
332 | 			mcpLog: {
333 | 				info: jest.fn(),
334 | 				warn: jest.fn(),
335 | 				error: jest.fn(),
336 | 				debug: jest.fn(),
337 | 				success: jest.fn()
338 | 			}
339 | 		});
340 | 
341 | 		// Assert
342 | 		expect(readJSON).toHaveBeenCalledWith(
343 | 			'tasks/tasks.json',
344 | 			'/mock/project/root',
345 | 			undefined
346 | 		);
347 | 		expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
348 | 		expect(mockWriteFileSync).toHaveBeenCalledWith(
349 | 			expect.stringContaining('task-complexity-report.json'),
350 | 			expect.stringContaining('"thresholdScore": 5'),
351 | 			'utf8'
352 | 		);
353 | 	});
354 | 
355 | 	test('should use research flag to determine which AI service to use', async () => {
356 | 		// Arrange
357 | 		const researchOptions = {
358 | 			file: 'tasks/tasks.json',
359 | 			output: 'scripts/task-complexity-report.json',
360 | 			threshold: '5',
361 | 			research: true,
362 | 			projectRoot: '/mock/project/root'
363 | 		};
364 | 
365 | 		// Act
366 | 		await analyzeTaskComplexity(researchOptions, {
367 | 			projectRoot: '/mock/project/root',
368 | 			mcpLog: {
369 | 				info: jest.fn(),
370 | 				warn: jest.fn(),
371 | 				error: jest.fn(),
372 | 				debug: jest.fn(),
373 | 				success: jest.fn()
374 | 			}
375 | 		});
376 | 
377 | 		// Assert
378 | 		expect(generateObjectService).toHaveBeenCalledWith(
379 | 			expect.objectContaining({
380 | 				role: 'research' // This should be present when research is true
381 | 			})
382 | 		);
383 | 	});
384 | 
385 | 	test('should handle different threshold parameter types correctly', async () => {
386 | 		// Test with string threshold
387 | 		let options = {
388 | 			file: 'tasks/tasks.json',
389 | 			output: 'scripts/task-complexity-report.json',
390 | 			threshold: '7',
391 | 			projectRoot: '/mock/project/root'
392 | 		};
393 | 
394 | 		await analyzeTaskComplexity(options, {
395 | 			projectRoot: '/mock/project/root',
396 | 			mcpLog: {
397 | 				info: jest.fn(),
398 | 				warn: jest.fn(),
399 | 				error: jest.fn(),
400 | 				debug: jest.fn(),
401 | 				success: jest.fn()
402 | 			}
403 | 		});
404 | 
405 | 		expect(mockWriteFileSync).toHaveBeenCalledWith(
406 | 			expect.stringContaining('task-complexity-report.json'),
407 | 			expect.stringContaining('"thresholdScore": 7'),
408 | 			'utf8'
409 | 		);
410 | 
411 | 		// Reset mocks
412 | 		jest.clearAllMocks();
413 | 
414 | 		// Test with number threshold
415 | 		options = {
416 | 			file: 'tasks/tasks.json',
417 | 			output: 'scripts/task-complexity-report.json',
418 | 			threshold: 8,
419 | 			projectRoot: '/mock/project/root'
420 | 		};
421 | 
422 | 		await analyzeTaskComplexity(options, {
423 | 			projectRoot: '/mock/project/root',
424 | 			mcpLog: {
425 | 				info: jest.fn(),
426 | 				warn: jest.fn(),
427 | 				error: jest.fn(),
428 | 				debug: jest.fn(),
429 | 				success: jest.fn()
430 | 			}
431 | 		});
432 | 
433 | 		expect(mockWriteFileSync).toHaveBeenCalledWith(
434 | 			expect.stringContaining('task-complexity-report.json'),
435 | 			expect.stringContaining('"thresholdScore": 8'),
436 | 			'utf8'
437 | 		);
438 | 	});
439 | 
440 | 	test('should filter out completed tasks from analysis', async () => {
441 | 		// Arrange
442 | 		const options = {
443 | 			file: 'tasks/tasks.json',
444 | 			output: 'scripts/task-complexity-report.json',
445 | 			threshold: '5',
446 | 			projectRoot: '/mock/project/root'
447 | 		};
448 | 
449 | 		// Act
450 | 		await analyzeTaskComplexity(options, {
451 | 			projectRoot: '/mock/project/root',
452 | 			mcpLog: {
453 | 				info: jest.fn(),
454 | 				warn: jest.fn(),
455 | 				error: jest.fn(),
456 | 				debug: jest.fn(),
457 | 				success: jest.fn()
458 | 			}
459 | 		});
460 | 
461 | 		// Assert
462 | 		// Check if the prompt sent to AI doesn't include the completed task (id: 3)
463 | 		expect(generateObjectService).toHaveBeenCalledWith(
464 | 			expect.objectContaining({
465 | 				prompt: expect.not.stringContaining('"id": 3')
466 | 			})
467 | 		);
468 | 	});
469 | 
470 | 	test('should handle API errors gracefully', async () => {
471 | 		// Arrange
472 | 		const options = {
473 | 			file: 'tasks/tasks.json',
474 | 			output: 'scripts/task-complexity-report.json',
475 | 			threshold: '5',
476 | 			projectRoot: '/mock/project/root'
477 | 		};
478 | 
479 | 		// Force API error
480 | 		generateObjectService.mockRejectedValueOnce(new Error('API Error'));
481 | 
482 | 		const mockMcpLog = {
483 | 			info: jest.fn(),
484 | 			warn: jest.fn(),
485 | 			error: jest.fn(),
486 | 			debug: jest.fn(),
487 | 			success: jest.fn()
488 | 		};
489 | 
490 | 		// Act & Assert
491 | 		await expect(
492 | 			analyzeTaskComplexity(options, {
493 | 				projectRoot: '/mock/project/root',
494 | 				mcpLog: mockMcpLog
495 | 			})
496 | 		).rejects.toThrow('API Error');
497 | 
498 | 		// Check that the error was logged via mcpLog
499 | 		expect(mockMcpLog.error).toHaveBeenCalledWith(
500 | 			expect.stringContaining('API Error')
501 | 		);
502 | 	});
503 | });
504 | 
```

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

```javascript
  1 | /**
  2 |  * Tests for models.js baseURL handling
  3 |  * Verifies that baseURL is only preserved when switching models within the same provider
  4 |  */
  5 | import { jest } from '@jest/globals';
  6 | 
  7 | // Mock the config manager
  8 | const mockConfigManager = {
  9 | 	getMainModelId: jest.fn(() => 'claude-3-sonnet-20240229'),
 10 | 	getResearchModelId: jest.fn(
 11 | 		() => 'perplexity-llama-3.1-sonar-large-128k-online'
 12 | 	),
 13 | 	getFallbackModelId: jest.fn(() => 'gpt-4o-mini'),
 14 | 	getMainProvider: jest.fn(),
 15 | 	getResearchProvider: jest.fn(),
 16 | 	getFallbackProvider: jest.fn(),
 17 | 	getBaseUrlForRole: jest.fn(),
 18 | 	getAvailableModels: jest.fn(),
 19 | 	getConfig: jest.fn(),
 20 | 	writeConfig: jest.fn(),
 21 | 	isConfigFilePresent: jest.fn(() => true),
 22 | 	getAllProviders: jest.fn(() => [
 23 | 		'anthropic',
 24 | 		'openai',
 25 | 		'google',
 26 | 		'openrouter'
 27 | 	]),
 28 | 	isApiKeySet: jest.fn(() => true),
 29 | 	getMcpApiKeyStatus: jest.fn(() => true)
 30 | };
 31 | 
 32 | jest.unstable_mockModule(
 33 | 	'../../../../../scripts/modules/config-manager.js',
 34 | 	() => mockConfigManager
 35 | );
 36 | 
 37 | // Mock path utils
 38 | jest.unstable_mockModule('../../../../../src/utils/path-utils.js', () => ({
 39 | 	findConfigPath: jest.fn(() => '/test/path/.taskmaster/config.json')
 40 | }));
 41 | 
 42 | // Mock utils
 43 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
 44 | 	log: jest.fn()
 45 | }));
 46 | 
 47 | // Mock core constants
 48 | jest.unstable_mockModule('@tm/core', () => ({
 49 | 	CUSTOM_PROVIDERS: {
 50 | 		OLLAMA: 'ollama',
 51 | 		LMSTUDIO: 'lmstudio',
 52 | 		OPENROUTER: 'openrouter',
 53 | 		BEDROCK: 'bedrock',
 54 | 		CLAUDE_CODE: 'claude-code',
 55 | 		AZURE: 'azure',
 56 | 		VERTEX: 'vertex',
 57 | 		GEMINI_CLI: 'gemini-cli',
 58 | 		CODEX_CLI: 'codex-cli',
 59 | 		OPENAI_COMPATIBLE: 'openai-compatible'
 60 | 	}
 61 | }));
 62 | 
 63 | // Import the module under test after mocks are set up
 64 | const { setModel } = await import(
 65 | 	'../../../../../scripts/modules/task-manager/models.js'
 66 | );
 67 | 
 68 | describe('models.js - baseURL handling for LMSTUDIO', () => {
 69 | 	const mockProjectRoot = '/test/project';
 70 | 	const mockConfig = {
 71 | 		models: {
 72 | 			main: { provider: 'lmstudio', modelId: 'existing-model' },
 73 | 			research: { provider: 'ollama', modelId: 'llama2' },
 74 | 			fallback: { provider: 'anthropic', modelId: 'claude-3-haiku-20240307' }
 75 | 		}
 76 | 	};
 77 | 
 78 | 	beforeEach(() => {
 79 | 		jest.clearAllMocks();
 80 | 		mockConfigManager.getConfig.mockReturnValue(
 81 | 			JSON.parse(JSON.stringify(mockConfig))
 82 | 		);
 83 | 		mockConfigManager.writeConfig.mockReturnValue(true);
 84 | 		mockConfigManager.getAvailableModels.mockReturnValue([]);
 85 | 	});
 86 | 
 87 | 	test('should use provided baseURL when explicitly given', async () => {
 88 | 		const customBaseURL = 'http://192.168.1.100:1234/v1';
 89 | 		mockConfigManager.getMainProvider.mockReturnValue('lmstudio');
 90 | 
 91 | 		const result = await setModel('main', 'custom-model', {
 92 | 			projectRoot: mockProjectRoot,
 93 | 			providerHint: 'lmstudio',
 94 | 			baseURL: customBaseURL
 95 | 		});
 96 | 
 97 | 		// Check if setModel succeeded
 98 | 		expect(result).toHaveProperty('success');
 99 | 		if (!result.success) {
100 | 			throw new Error(`setModel failed: ${JSON.stringify(result.error)}`);
101 | 		}
102 | 
103 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
104 | 		expect(writtenConfig.models.main.baseURL).toBe(customBaseURL);
105 | 	});
106 | 
107 | 	test('should preserve existing baseURL when already using LMSTUDIO', async () => {
108 | 		const existingBaseURL = 'http://custom-lmstudio:8080/v1';
109 | 		mockConfigManager.getMainProvider.mockReturnValue('lmstudio');
110 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(existingBaseURL);
111 | 
112 | 		await setModel('main', 'new-lmstudio-model', {
113 | 			projectRoot: mockProjectRoot,
114 | 			providerHint: 'lmstudio'
115 | 		});
116 | 
117 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
118 | 		expect(writtenConfig.models.main.baseURL).toBe(existingBaseURL);
119 | 	});
120 | 
121 | 	test('should use default baseURL when switching from OLLAMA to LMSTUDIO', async () => {
122 | 		const ollamaBaseURL = 'http://ollama-server:11434/api';
123 | 		mockConfigManager.getMainProvider.mockReturnValue('ollama');
124 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(ollamaBaseURL);
125 | 
126 | 		await setModel('main', 'lmstudio-model', {
127 | 			projectRoot: mockProjectRoot,
128 | 			providerHint: 'lmstudio'
129 | 		});
130 | 
131 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
132 | 		// Should use default LMSTUDIO baseURL, not OLLAMA's
133 | 		expect(writtenConfig.models.main.baseURL).toBe('http://localhost:1234/v1');
134 | 		expect(writtenConfig.models.main.baseURL).not.toBe(ollamaBaseURL);
135 | 	});
136 | 
137 | 	test('should use default baseURL when switching from any other provider to LMSTUDIO', async () => {
138 | 		mockConfigManager.getMainProvider.mockReturnValue('anthropic');
139 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(null);
140 | 
141 | 		await setModel('main', 'lmstudio-model', {
142 | 			projectRoot: mockProjectRoot,
143 | 			providerHint: 'lmstudio'
144 | 		});
145 | 
146 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
147 | 		expect(writtenConfig.models.main.baseURL).toBe('http://localhost:1234/v1');
148 | 	});
149 | });
150 | 
151 | // NOTE: OLLAMA tests omitted since they require HTTP mocking for fetchOllamaModels.
152 | // The baseURL preservation logic is identical to LMSTUDIO, so LMSTUDIO tests prove it works.
153 | 
154 | describe.skip('models.js - baseURL handling for OLLAMA', () => {
155 | 	const mockProjectRoot = '/test/project';
156 | 	const mockConfig = {
157 | 		models: {
158 | 			main: { provider: 'ollama', modelId: 'existing-model' },
159 | 			research: { provider: 'lmstudio', modelId: 'some-model' },
160 | 			fallback: { provider: 'anthropic', modelId: 'claude-3-haiku-20240307' }
161 | 		}
162 | 	};
163 | 
164 | 	beforeEach(() => {
165 | 		jest.clearAllMocks();
166 | 		mockConfigManager.getConfig.mockReturnValue(
167 | 			JSON.parse(JSON.stringify(mockConfig))
168 | 		);
169 | 		mockConfigManager.writeConfig.mockReturnValue(true);
170 | 		mockConfigManager.getAvailableModels.mockReturnValue([]);
171 | 	});
172 | 
173 | 	test('should use provided baseURL when explicitly given', async () => {
174 | 		const customBaseURL = 'http://192.168.1.200:11434/api';
175 | 		mockConfigManager.getMainProvider.mockReturnValue('ollama');
176 | 
177 | 		// Mock fetch for Ollama models check
178 | 		global.fetch = jest.fn(() =>
179 | 			Promise.resolve({
180 | 				ok: true,
181 | 				json: () => Promise.resolve({ models: [{ model: 'custom-model' }] })
182 | 			})
183 | 		);
184 | 
185 | 		await setModel('main', 'custom-model', {
186 | 			projectRoot: mockProjectRoot,
187 | 			providerHint: 'ollama',
188 | 			baseURL: customBaseURL
189 | 		});
190 | 
191 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
192 | 		expect(writtenConfig.models.main.baseURL).toBe(customBaseURL);
193 | 	});
194 | 
195 | 	test('should preserve existing baseURL when already using OLLAMA', async () => {
196 | 		const existingBaseURL = 'http://custom-ollama:9999/api';
197 | 		mockConfigManager.getMainProvider.mockReturnValue('ollama');
198 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(existingBaseURL);
199 | 
200 | 		// Mock fetch for Ollama models check
201 | 		global.fetch = jest.fn(() =>
202 | 			Promise.resolve({
203 | 				ok: true,
204 | 				json: () => Promise.resolve({ models: [{ model: 'new-ollama-model' }] })
205 | 			})
206 | 		);
207 | 
208 | 		await setModel('main', 'new-ollama-model', {
209 | 			projectRoot: mockProjectRoot,
210 | 			providerHint: 'ollama'
211 | 		});
212 | 
213 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
214 | 		expect(writtenConfig.models.main.baseURL).toBe(existingBaseURL);
215 | 	});
216 | 
217 | 	test('should use default baseURL when switching from LMSTUDIO to OLLAMA', async () => {
218 | 		const lmstudioBaseURL = 'http://lmstudio-server:1234/v1';
219 | 		mockConfigManager.getMainProvider.mockReturnValue('lmstudio');
220 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(lmstudioBaseURL);
221 | 
222 | 		// Mock fetch for Ollama models check
223 | 		global.fetch = jest.fn(() =>
224 | 			Promise.resolve({
225 | 				ok: true,
226 | 				json: () => Promise.resolve({ models: [{ model: 'ollama-model' }] })
227 | 			})
228 | 		);
229 | 
230 | 		await setModel('main', 'ollama-model', {
231 | 			projectRoot: mockProjectRoot,
232 | 			providerHint: 'ollama'
233 | 		});
234 | 
235 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
236 | 		// Should use default OLLAMA baseURL, not LMSTUDIO's
237 | 		expect(writtenConfig.models.main.baseURL).toBe(
238 | 			'http://localhost:11434/api'
239 | 		);
240 | 		expect(writtenConfig.models.main.baseURL).not.toBe(lmstudioBaseURL);
241 | 	});
242 | 
243 | 	test('should use default baseURL when switching from any other provider to OLLAMA', async () => {
244 | 		mockConfigManager.getMainProvider.mockReturnValue('anthropic');
245 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(null);
246 | 
247 | 		// Mock fetch for Ollama models check
248 | 		global.fetch = jest.fn(() =>
249 | 			Promise.resolve({
250 | 				ok: true,
251 | 				json: () => Promise.resolve({ models: [{ model: 'ollama-model' }] })
252 | 			})
253 | 		);
254 | 
255 | 		await setModel('main', 'ollama-model', {
256 | 			projectRoot: mockProjectRoot,
257 | 			providerHint: 'ollama'
258 | 		});
259 | 
260 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
261 | 		expect(writtenConfig.models.main.baseURL).toBe(
262 | 			'http://localhost:11434/api'
263 | 		);
264 | 	});
265 | });
266 | 
267 | describe.skip('models.js - cross-provider baseURL isolation', () => {
268 | 	const mockProjectRoot = '/test/project';
269 | 	const mockConfig = {
270 | 		models: {
271 | 			main: {
272 | 				provider: 'ollama',
273 | 				modelId: 'existing-model',
274 | 				baseURL: 'http://ollama:11434/api'
275 | 			},
276 | 			research: {
277 | 				provider: 'lmstudio',
278 | 				modelId: 'some-model',
279 | 				baseURL: 'http://lmstudio:1234/v1'
280 | 			},
281 | 			fallback: { provider: 'anthropic', modelId: 'claude-3-haiku-20240307' }
282 | 		}
283 | 	};
284 | 
285 | 	beforeEach(() => {
286 | 		jest.clearAllMocks();
287 | 		mockConfigManager.getConfig.mockReturnValue(
288 | 			JSON.parse(JSON.stringify(mockConfig))
289 | 		);
290 | 		mockConfigManager.writeConfig.mockReturnValue(true);
291 | 		mockConfigManager.getAvailableModels.mockReturnValue([]);
292 | 	});
293 | 
294 | 	test('OLLAMA baseURL should not leak to LMSTUDIO', async () => {
295 | 		const ollamaBaseURL = 'http://custom-ollama:11434/api';
296 | 		mockConfigManager.getMainProvider.mockReturnValue('ollama');
297 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(ollamaBaseURL);
298 | 
299 | 		await setModel('main', 'lmstudio-model', {
300 | 			projectRoot: mockProjectRoot,
301 | 			providerHint: 'lmstudio'
302 | 		});
303 | 
304 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
305 | 		expect(writtenConfig.models.main.provider).toBe('lmstudio');
306 | 		expect(writtenConfig.models.main.baseURL).toBe('http://localhost:1234/v1');
307 | 		expect(writtenConfig.models.main.baseURL).not.toContain('ollama');
308 | 	});
309 | 
310 | 	test('LMSTUDIO baseURL should not leak to OLLAMA', async () => {
311 | 		const lmstudioBaseURL = 'http://custom-lmstudio:1234/v1';
312 | 		mockConfigManager.getMainProvider.mockReturnValue('lmstudio');
313 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(lmstudioBaseURL);
314 | 
315 | 		// Mock fetch for Ollama models check
316 | 		global.fetch = jest.fn(() =>
317 | 			Promise.resolve({
318 | 				ok: true,
319 | 				json: () => Promise.resolve({ models: [{ model: 'ollama-model' }] })
320 | 			})
321 | 		);
322 | 
323 | 		await setModel('main', 'ollama-model', {
324 | 			projectRoot: mockProjectRoot,
325 | 			providerHint: 'ollama'
326 | 		});
327 | 
328 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
329 | 		expect(writtenConfig.models.main.provider).toBe('ollama');
330 | 		expect(writtenConfig.models.main.baseURL).toBe(
331 | 			'http://localhost:11434/api'
332 | 		);
333 | 		expect(writtenConfig.models.main.baseURL).not.toContain('lmstudio');
334 | 		expect(writtenConfig.models.main.baseURL).not.toContain('1234');
335 | 	});
336 | });
337 | 
338 | describe('models.js - baseURL handling for OPENAI_COMPATIBLE', () => {
339 | 	const mockProjectRoot = '/test/project';
340 | 	const mockConfig = {
341 | 		models: {
342 | 			main: {
343 | 				provider: 'openai-compatible',
344 | 				modelId: 'existing-model',
345 | 				baseURL: 'https://api.custom.com/v1'
346 | 			},
347 | 			research: { provider: 'anthropic', modelId: 'claude-3-haiku-20240307' },
348 | 			fallback: { provider: 'openai', modelId: 'gpt-4o-mini' }
349 | 		}
350 | 	};
351 | 
352 | 	beforeEach(() => {
353 | 		jest.clearAllMocks();
354 | 		mockConfigManager.getConfig.mockReturnValue(
355 | 			JSON.parse(JSON.stringify(mockConfig))
356 | 		);
357 | 		mockConfigManager.writeConfig.mockReturnValue(true);
358 | 		mockConfigManager.getAvailableModels.mockReturnValue([]);
359 | 	});
360 | 
361 | 	test('should preserve existing baseURL when already using OPENAI_COMPATIBLE', async () => {
362 | 		const existingBaseURL = 'https://api.custom.com/v1';
363 | 		mockConfigManager.getMainProvider.mockReturnValue('openai-compatible');
364 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(existingBaseURL);
365 | 
366 | 		const result = await setModel('main', 'new-compatible-model', {
367 | 			projectRoot: mockProjectRoot,
368 | 			providerHint: 'openai-compatible'
369 | 		});
370 | 
371 | 		expect(result).toHaveProperty('success');
372 | 		if (!result.success) {
373 | 			throw new Error(`setModel failed: ${JSON.stringify(result.error)}`);
374 | 		}
375 | 
376 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
377 | 		expect(writtenConfig.models.main.baseURL).toBe(existingBaseURL);
378 | 	});
379 | 
380 | 	test('should require baseURL when switching from another provider to OPENAI_COMPATIBLE', async () => {
381 | 		mockConfigManager.getMainProvider.mockReturnValue('anthropic');
382 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(null);
383 | 
384 | 		const result = await setModel('main', 'compatible-model', {
385 | 			projectRoot: mockProjectRoot,
386 | 			providerHint: 'openai-compatible'
387 | 			// No baseURL provided
388 | 		});
389 | 
390 | 		expect(result.success).toBe(false);
391 | 		expect(result.error?.message).toContain(
392 | 			'Base URL is required for OpenAI-compatible providers'
393 | 		);
394 | 	});
395 | 
396 | 	test('should use provided baseURL when switching to OPENAI_COMPATIBLE', async () => {
397 | 		const newBaseURL = 'https://api.newprovider.com/v1';
398 | 		mockConfigManager.getMainProvider.mockReturnValue('anthropic');
399 | 		mockConfigManager.getBaseUrlForRole.mockReturnValue(null);
400 | 
401 | 		const result = await setModel('main', 'compatible-model', {
402 | 			projectRoot: mockProjectRoot,
403 | 			providerHint: 'openai-compatible',
404 | 			baseURL: newBaseURL
405 | 		});
406 | 
407 | 		expect(result).toHaveProperty('success');
408 | 		if (!result.success) {
409 | 			throw new Error(`setModel failed: ${JSON.stringify(result.error)}`);
410 | 		}
411 | 
412 | 		const writtenConfig = mockConfigManager.writeConfig.mock.calls[0][0];
413 | 		expect(writtenConfig.models.main.baseURL).toBe(newBaseURL);
414 | 	});
415 | });
416 | 
```

--------------------------------------------------------------------------------
/apps/extension/src/services/webview-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Webview Manager - Simplified
  3 |  * Manages webview panels and message handling
  4 |  */
  5 | 
  6 | import * as vscode from 'vscode';
  7 | import type { EventEmitter } from '../utils/event-emitter';
  8 | import type { ExtensionLogger } from '../utils/logger';
  9 | import type { ConfigService } from './config-service';
 10 | import type { TaskRepository } from './task-repository';
 11 | import type { TerminalManager } from './terminal-manager';
 12 | 
 13 | export class WebviewManager {
 14 | 	private panels = new Set<vscode.WebviewPanel>();
 15 | 	private configService?: ConfigService;
 16 | 	private mcpClient?: any;
 17 | 	private api?: any;
 18 | 
 19 | 	constructor(
 20 | 		private context: vscode.ExtensionContext,
 21 | 		private repository: TaskRepository,
 22 | 		private events: EventEmitter,
 23 | 		private logger: ExtensionLogger,
 24 | 		private terminalManager: TerminalManager
 25 | 	) {}
 26 | 
 27 | 	setConfigService(configService: ConfigService): void {
 28 | 		this.configService = configService;
 29 | 	}
 30 | 
 31 | 	setMCPClient(mcpClient: any): void {
 32 | 		this.mcpClient = mcpClient;
 33 | 	}
 34 | 
 35 | 	setApi(api: any): void {
 36 | 		this.api = api;
 37 | 	}
 38 | 
 39 | 	async createOrShowPanel(): Promise<void> {
 40 | 		// Find existing panel
 41 | 		const existing = Array.from(this.panels).find(
 42 | 			(p) => p.title === 'TaskMaster Kanban'
 43 | 		);
 44 | 		if (existing) {
 45 | 			existing.reveal();
 46 | 			return;
 47 | 		}
 48 | 
 49 | 		// Create new panel
 50 | 		const panel = vscode.window.createWebviewPanel(
 51 | 			'taskrKanban',
 52 | 			'TaskMaster Kanban',
 53 | 			vscode.ViewColumn.One,
 54 | 			{
 55 | 				enableScripts: true,
 56 | 				retainContextWhenHidden: true,
 57 | 				localResourceRoots: [
 58 | 					vscode.Uri.joinPath(this.context.extensionUri, 'dist')
 59 | 				]
 60 | 			}
 61 | 		);
 62 | 
 63 | 		// Set the icon for the webview tab
 64 | 		panel.iconPath = {
 65 | 			light: vscode.Uri.joinPath(
 66 | 				this.context.extensionUri,
 67 | 				'assets',
 68 | 				'icon-light.svg'
 69 | 			),
 70 | 			dark: vscode.Uri.joinPath(
 71 | 				this.context.extensionUri,
 72 | 				'assets',
 73 | 				'icon-dark.svg'
 74 | 			)
 75 | 		};
 76 | 
 77 | 		this.panels.add(panel);
 78 | 		panel.webview.html = this.getWebviewContent(panel.webview);
 79 | 
 80 | 		// Handle messages
 81 | 		panel.webview.onDidReceiveMessage(async (message) => {
 82 | 			await this.handleMessage(panel, message);
 83 | 		});
 84 | 
 85 | 		// Handle disposal
 86 | 		panel.onDidDispose(() => {
 87 | 			this.panels.delete(panel);
 88 | 			this.events.emit('webview:closed');
 89 | 		});
 90 | 
 91 | 		this.events.emit('webview:opened');
 92 | 		vscode.window.showInformationMessage('TaskMaster Kanban opened!');
 93 | 	}
 94 | 
 95 | 	broadcast(type: string, data: any): void {
 96 | 		this.panels.forEach((panel) => {
 97 | 			panel.webview.postMessage({ type, data });
 98 | 		});
 99 | 	}
100 | 
101 | 	getPanelCount(): number {
102 | 		return this.panels.size;
103 | 	}
104 | 
105 | 	dispose(): void {
106 | 		this.panels.forEach((panel) => panel.dispose());
107 | 		this.panels.clear();
108 | 	}
109 | 
110 | 	private async handleMessage(
111 | 		panel: vscode.WebviewPanel,
112 | 		message: any
113 | 	): Promise<void> {
114 | 		// Validate message structure
115 | 		if (!message || typeof message !== 'object') {
116 | 			this.logger.error('Invalid message received:', message);
117 | 			return;
118 | 		}
119 | 
120 | 		const { type, data, requestId } = message;
121 | 		this.logger.debug(`Webview message: ${type}`, message);
122 | 
123 | 		try {
124 | 			let response: any;
125 | 
126 | 			switch (type) {
127 | 				case 'ready':
128 | 					// Webview is ready, send current connection status
129 | 					const isConnected = this.mcpClient?.getStatus()?.isRunning || false;
130 | 					panel.webview.postMessage({
131 | 						type: 'connectionStatus',
132 | 						data: {
133 | 							isConnected: isConnected,
134 | 							status: isConnected ? 'Connected' : 'Disconnected'
135 | 						}
136 | 					});
137 | 					// No response needed for ready message
138 | 					return;
139 | 
140 | 				case 'getTasks':
141 | 					// Pass options to getAll including tag if specified
142 | 					response = await this.repository.getAll({
143 | 						tag: data?.tag,
144 | 						withSubtasks: data?.withSubtasks ?? true
145 | 					});
146 | 					break;
147 | 
148 | 				case 'updateTaskStatus':
149 | 					await this.repository.updateStatus(data.taskId, data.newStatus);
150 | 					response = { success: true };
151 | 					break;
152 | 
153 | 				case 'getConfig':
154 | 					if (this.configService) {
155 | 						response = await this.configService.getSafeConfig();
156 | 					} else {
157 | 						response = null;
158 | 					}
159 | 					break;
160 | 
161 | 				case 'readTaskFileData':
162 | 					// For now, return the task data from repository
163 | 					// In the future, this could read from actual task files
164 | 					const task = await this.repository.getById(data.taskId);
165 | 					if (task) {
166 | 						response = {
167 | 							details: task.details || '',
168 | 							testStrategy: task.testStrategy || ''
169 | 						};
170 | 					} else {
171 | 						response = {
172 | 							details: '',
173 | 							testStrategy: ''
174 | 						};
175 | 					}
176 | 					break;
177 | 
178 | 				case 'updateTask':
179 | 					// Handle task content updates with MCP
180 | 					if (this.mcpClient) {
181 | 						try {
182 | 							const { taskId, updates, options = {} } = data;
183 | 
184 | 							// Use the update_task MCP tool
185 | 							await this.mcpClient.callTool('update_task', {
186 | 								id: String(taskId),
187 | 								prompt: updates.description || '',
188 | 								append: options.append || false,
189 | 								research: options.research || false,
190 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
191 | 							});
192 | 
193 | 							response = { success: true };
194 | 						} catch (error) {
195 | 							this.logger.error('Failed to update task via MCP:', error);
196 | 							throw error;
197 | 						}
198 | 					} else {
199 | 						throw new Error('MCP client not initialized');
200 | 					}
201 | 					break;
202 | 
203 | 				case 'updateSubtask':
204 | 					// Handle subtask content updates with MCP
205 | 					if (this.mcpClient) {
206 | 						try {
207 | 							const { taskId, prompt, options = {} } = data;
208 | 
209 | 							// Use the update_subtask MCP tool
210 | 							await this.mcpClient.callTool('update_subtask', {
211 | 								id: String(taskId),
212 | 								prompt: prompt,
213 | 								research: options.research || false,
214 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
215 | 							});
216 | 
217 | 							response = { success: true };
218 | 						} catch (error) {
219 | 							this.logger.error('Failed to update subtask via MCP:', error);
220 | 							throw error;
221 | 						}
222 | 					} else {
223 | 						throw new Error('MCP client not initialized');
224 | 					}
225 | 					break;
226 | 
227 | 				case 'getComplexity':
228 | 					// For backward compatibility - redirect to mcpRequest
229 | 					this.logger.debug(
230 | 						`getComplexity request for task ${data.taskId}, mcpClient available: ${!!this.mcpClient}`
231 | 					);
232 | 					if (this.mcpClient && data.taskId) {
233 | 						try {
234 | 							const complexityResult = await this.mcpClient.callTool(
235 | 								'complexity_report',
236 | 								{
237 | 									projectRoot:
238 | 										vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
239 | 								}
240 | 							);
241 | 
242 | 							if (complexityResult?.report?.complexityAnalysis?.tasks) {
243 | 								const task =
244 | 									complexityResult.report.complexityAnalysis.tasks.find(
245 | 										(t: any) => t.id === data.taskId
246 | 									);
247 | 								response = task ? { score: task.complexityScore } : {};
248 | 							} else {
249 | 								response = {};
250 | 							}
251 | 						} catch (error) {
252 | 							this.logger.error('Failed to get complexity', error);
253 | 							response = {};
254 | 						}
255 | 					} else {
256 | 						this.logger.warn(
257 | 							`Cannot get complexity: mcpClient=${!!this.mcpClient}, taskId=${data.taskId}`
258 | 						);
259 | 						response = {};
260 | 					}
261 | 					break;
262 | 
263 | 				case 'mcpRequest':
264 | 					// Handle MCP tool calls
265 | 					try {
266 | 						// The tool and params come directly in the message
267 | 						const tool = message.tool;
268 | 						const params = message.params || {};
269 | 
270 | 						if (!this.mcpClient) {
271 | 							throw new Error('MCP client not initialized');
272 | 						}
273 | 
274 | 						if (!tool) {
275 | 							throw new Error('Tool name not specified in mcpRequest');
276 | 						}
277 | 
278 | 						// Add projectRoot if not provided
279 | 						if (!params.projectRoot) {
280 | 							params.projectRoot =
281 | 								vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
282 | 						}
283 | 
284 | 						const result = await this.mcpClient.callTool(tool, params);
285 | 						response = { data: result };
286 | 					} catch (error) {
287 | 						this.logger.error('MCP request failed:', error);
288 | 						// Re-throw with cleaner error message
289 | 						throw new Error(
290 | 							error instanceof Error ? error.message : 'Unknown error'
291 | 						);
292 | 					}
293 | 					break;
294 | 
295 | 				case 'getTags':
296 | 					// Get available tags
297 | 					if (this.mcpClient) {
298 | 						try {
299 | 							const result = await this.mcpClient.callTool('list_tags', {
300 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
301 | 								showMetadata: false
302 | 							});
303 | 							// The MCP response has a specific structure
304 | 							// Based on the MCP SDK, the response is in result.content[0].text
305 | 							let parsedData;
306 | 							if (
307 | 								result?.content &&
308 | 								Array.isArray(result.content) &&
309 | 								result.content[0]?.text
310 | 							) {
311 | 								try {
312 | 									parsedData = JSON.parse(result.content[0].text);
313 | 								} catch (e) {
314 | 									this.logger.error('Failed to parse MCP response text:', e);
315 | 								}
316 | 							}
317 | 
318 | 							// Extract tags data from the parsed response
319 | 							if (parsedData?.data) {
320 | 								response = parsedData.data;
321 | 							} else if (parsedData) {
322 | 								response = parsedData;
323 | 							} else if (result?.data) {
324 | 								response = result.data;
325 | 							} else {
326 | 								response = { tags: [], currentTag: 'master' };
327 | 							}
328 | 						} catch (error) {
329 | 							this.logger.error('Failed to get tags:', error);
330 | 							response = { tags: [], currentTag: 'master' };
331 | 						}
332 | 					} else {
333 | 						response = { tags: [], currentTag: 'master' };
334 | 					}
335 | 					break;
336 | 
337 | 				case 'switchTag':
338 | 					// Switch to a different tag
339 | 					if (this.mcpClient && data.tagName) {
340 | 						try {
341 | 							await this.mcpClient.callTool('use_tag', {
342 | 								name: data.tagName,
343 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
344 | 							});
345 | 							// Clear cache and fetch tasks for the new tag
346 | 							await this.repository.refresh();
347 | 							const tasks = await this.repository.getAll({ tag: data.tagName });
348 | 							this.broadcast('tasksUpdated', { tasks, source: 'tag-switch' });
349 | 							response = { success: true };
350 | 						} catch (error) {
351 | 							this.logger.error('Failed to switch tag:', error);
352 | 							throw error;
353 | 						}
354 | 					} else {
355 | 						throw new Error('Tag name not provided');
356 | 					}
357 | 					break;
358 | 
359 | 				case 'openExternal':
360 | 					// Open external URL
361 | 					if (message.url) {
362 | 						vscode.env.openExternal(vscode.Uri.parse(message.url));
363 | 					}
364 | 					return;
365 | 
366 | 				case 'openTerminal':
367 | 					// Delegate terminal execution to TerminalManager
368 | 					const { taskId, taskTitle } = data.data || data; // Handle both nested and direct data
369 | 					this.logger.log(
370 | 						`Webview openTerminal - taskId: ${taskId} (type: ${typeof taskId}), taskTitle: ${taskTitle}`
371 | 					);
372 | 
373 | 					// Get current tag to ensure we're working in the right context
374 | 					let currentTag = 'master'; // default fallback
375 | 					if (this.mcpClient) {
376 | 						try {
377 | 							const tagsResult = await this.mcpClient.callTool('list_tags', {
378 | 								projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
379 | 								showMetadata: false
380 | 							});
381 | 
382 | 							let parsedData;
383 | 							if (
384 | 								tagsResult?.content &&
385 | 								Array.isArray(tagsResult.content) &&
386 | 								tagsResult.content[0]?.text
387 | 							) {
388 | 								try {
389 | 									parsedData = JSON.parse(tagsResult.content[0].text);
390 | 									if (parsedData?.data?.currentTag) {
391 | 										currentTag = parsedData.data.currentTag;
392 | 									}
393 | 								} catch (e) {
394 | 									this.logger.warn(
395 | 										'Failed to parse tags response for terminal execution'
396 | 									);
397 | 								}
398 | 							}
399 | 						} catch (error) {
400 | 							this.logger.warn(
401 | 								'Failed to get current tag for terminal execution:',
402 | 								error
403 | 							);
404 | 						}
405 | 					}
406 | 
407 | 					const result = await this.terminalManager.executeTask({
408 | 						taskId,
409 | 						taskTitle,
410 | 						tag: currentTag
411 | 					});
412 | 
413 | 					response = result;
414 | 
415 | 					// Show user feedback AFTER sending the response (like the working "TaskMaster connected!" example)
416 | 					setImmediate(() => {
417 | 						if (result.success) {
418 | 							// Success: Show info message
419 | 							vscode.window.showInformationMessage(
420 | 								`✅ Started Claude session for Task ${taskId}: ${taskTitle}`
421 | 							);
422 | 						} else {
423 | 							// Error: Show VS Code native error notification only
424 | 							const errorMsg = `Failed to start task: ${result.error}`;
425 | 							vscode.window.showErrorMessage(errorMsg);
426 | 						}
427 | 					});
428 | 					break;
429 | 
430 | 				default:
431 | 					throw new Error(`Unknown message type: ${type}`);
432 | 			}
433 | 
434 | 			// Send response
435 | 			if (requestId) {
436 | 				panel.webview.postMessage({
437 | 					type: 'response',
438 | 					requestId,
439 | 					success: true,
440 | 					data: response
441 | 				});
442 | 			}
443 | 		} catch (error) {
444 | 			this.logger.error(`Error handling message ${type}`, error);
445 | 
446 | 			if (requestId) {
447 | 				panel.webview.postMessage({
448 | 					type: 'error',
449 | 					requestId,
450 | 					error: error instanceof Error ? error.message : 'Unknown error'
451 | 				});
452 | 			}
453 | 		}
454 | 	}
455 | 
456 | 	private getWebviewContent(webview: vscode.Webview): string {
457 | 		const scriptUri = webview.asWebviewUri(
458 | 			vscode.Uri.joinPath(this.context.extensionUri, 'dist', 'index.js')
459 | 		);
460 | 		const styleUri = webview.asWebviewUri(
461 | 			vscode.Uri.joinPath(this.context.extensionUri, 'dist', 'index.css')
462 | 		);
463 | 		const nonce = this.getNonce();
464 | 
465 | 		return `<!DOCTYPE html>
466 | <html lang="en">
467 | <head>
468 | 	<meta charset="UTF-8">
469 | 	<meta name="viewport" content="width=device-width, initial-scale=1.0">
470 | 	<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src ${webview.cspSource} https:; script-src 'nonce-${nonce}'; style-src ${webview.cspSource} 'unsafe-inline';">
471 | 	<link href="${styleUri}" rel="stylesheet">
472 | 	<title>TaskMaster Kanban</title>
473 | </head>
474 | <body>
475 | 	<div id="root"></div>
476 | 	<script nonce="${nonce}" src="${scriptUri}"></script>
477 | </body>
478 | </html>`;
479 | 	}
480 | 
481 | 	private getNonce(): string {
482 | 		let text = '';
483 | 		const possible =
484 | 			'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
485 | 		for (let i = 0; i < 32; i++) {
486 | 			text += possible.charAt(Math.floor(Math.random() * possible.length));
487 | 		}
488 | 		return text;
489 | 	}
490 | }
491 | 
```

--------------------------------------------------------------------------------
/tests/unit/initialize-project.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | import fs from 'fs';
  3 | import path from 'path';
  4 | import os from 'os';
  5 | 
  6 | // Reduce noise in test output
  7 | process.env.TASKMASTER_LOG_LEVEL = 'error';
  8 | 
  9 | // === Mock everything early ===
 10 | jest.mock('child_process', () => ({ execSync: jest.fn() }));
 11 | jest.mock('fs', () => ({
 12 | 	...jest.requireActual('fs'),
 13 | 	mkdirSync: jest.fn(),
 14 | 	writeFileSync: jest.fn(),
 15 | 	readFileSync: jest.fn(),
 16 | 	appendFileSync: jest.fn(),
 17 | 	existsSync: jest.fn(),
 18 | 	mkdtempSync: jest.requireActual('fs').mkdtempSync,
 19 | 	rmSync: jest.requireActual('fs').rmSync
 20 | }));
 21 | 
 22 | // Mock console methods to suppress output
 23 | const consoleMethods = ['log', 'info', 'warn', 'error', 'clear'];
 24 | consoleMethods.forEach((method) => {
 25 | 	global.console[method] = jest.fn();
 26 | });
 27 | 
 28 | // Mock ES modules using unstable_mockModule
 29 | jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
 30 | 	isSilentMode: jest.fn(() => true),
 31 | 	enableSilentMode: jest.fn(),
 32 | 	log: jest.fn(),
 33 | 	findProjectRoot: jest.fn(() => process.cwd())
 34 | }));
 35 | 
 36 | // Mock git-utils module
 37 | jest.unstable_mockModule('../../scripts/modules/utils/git-utils.js', () => ({
 38 | 	insideGitWorkTree: jest.fn(() => false)
 39 | }));
 40 | 
 41 | // Mock rule transformer
 42 | jest.unstable_mockModule('../../src/utils/rule-transformer.js', () => ({
 43 | 	convertAllRulesToProfileRules: jest.fn(),
 44 | 	getRulesProfile: jest.fn(() => ({
 45 | 		conversionConfig: {},
 46 | 		globalReplacements: []
 47 | 	}))
 48 | }));
 49 | 
 50 | // Mock any other modules that might output or do real operations
 51 | jest.unstable_mockModule('../../scripts/modules/config-manager.js', () => ({
 52 | 	createDefaultConfig: jest.fn(() => ({ models: {}, project: {} })),
 53 | 	saveConfig: jest.fn()
 54 | }));
 55 | 
 56 | // Mock display libraries
 57 | jest.mock('figlet', () => ({ textSync: jest.fn(() => 'MOCKED BANNER') }));
 58 | jest.mock('boxen', () => jest.fn(() => 'MOCKED BOX'));
 59 | jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
 60 | jest.mock('chalk', () => ({
 61 | 	blue: jest.fn((text) => text),
 62 | 	green: jest.fn((text) => text),
 63 | 	red: jest.fn((text) => text),
 64 | 	yellow: jest.fn((text) => text),
 65 | 	cyan: jest.fn((text) => text),
 66 | 	white: jest.fn((text) => text),
 67 | 	dim: jest.fn((text) => text),
 68 | 	bold: jest.fn((text) => text),
 69 | 	underline: jest.fn((text) => text)
 70 | }));
 71 | 
 72 | const { execSync } = jest.requireMock('child_process');
 73 | const mockFs = jest.requireMock('fs');
 74 | 
 75 | // Import the mocked modules
 76 | const mockUtils = await import('../../scripts/modules/utils.js');
 77 | const mockGitUtils = await import('../../scripts/modules/utils/git-utils.js');
 78 | const mockRuleTransformer = await import('../../src/utils/rule-transformer.js');
 79 | 
 80 | // Import after mocks
 81 | const { initializeProject } = await import('../../scripts/init.js');
 82 | 
 83 | describe('initializeProject – Git / Alias flag logic', () => {
 84 | 	let tmpDir;
 85 | 	const origCwd = process.cwd();
 86 | 
 87 | 	// Standard non-interactive options for all tests
 88 | 	const baseOptions = {
 89 | 		yes: true,
 90 | 		skipInstall: true,
 91 | 		name: 'test-project',
 92 | 		description: 'Test project description',
 93 | 		version: '1.0.0',
 94 | 		author: 'Test Author'
 95 | 	};
 96 | 
 97 | 	beforeEach(() => {
 98 | 		jest.clearAllMocks();
 99 | 
100 | 		// Set up basic fs mocks
101 | 		mockFs.mkdirSync.mockImplementation(() => {});
102 | 		mockFs.writeFileSync.mockImplementation(() => {});
103 | 		mockFs.readFileSync.mockImplementation((filePath) => {
104 | 			if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
105 | 				return 'mock template content';
106 | 			}
107 | 			if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
108 | 				return '# existing config';
109 | 			}
110 | 			return '';
111 | 		});
112 | 		mockFs.appendFileSync.mockImplementation(() => {});
113 | 		mockFs.existsSync.mockImplementation((filePath) => {
114 | 			// Template source files exist
115 | 			if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
116 | 				return true;
117 | 			}
118 | 			// Shell config files exist by default
119 | 			if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
120 | 				return true;
121 | 			}
122 | 			return false;
123 | 		});
124 | 
125 | 		// Reset utils mocks
126 | 		mockUtils.isSilentMode.mockReturnValue(true);
127 | 		mockGitUtils.insideGitWorkTree.mockReturnValue(false);
128 | 
129 | 		// Default execSync mock
130 | 		execSync.mockImplementation(() => '');
131 | 
132 | 		tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tm-init-'));
133 | 		process.chdir(tmpDir);
134 | 	});
135 | 
136 | 	afterEach(() => {
137 | 		process.chdir(origCwd);
138 | 		fs.rmSync(tmpDir, { recursive: true, force: true });
139 | 	});
140 | 
141 | 	describe('Git Flag Behavior', () => {
142 | 		it('completes successfully with git:false in dry run', async () => {
143 | 			const result = await initializeProject({
144 | 				...baseOptions,
145 | 				git: false,
146 | 				aliases: false,
147 | 				dryRun: true
148 | 			});
149 | 
150 | 			expect(result.dryRun).toBe(true);
151 | 		});
152 | 
153 | 		it('completes successfully with git:true when not inside repo', async () => {
154 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(false);
155 | 
156 | 			await expect(
157 | 				initializeProject({
158 | 					...baseOptions,
159 | 					git: true,
160 | 					aliases: false,
161 | 					dryRun: false
162 | 				})
163 | 			).resolves.not.toThrow();
164 | 		});
165 | 
166 | 		it('completes successfully when already inside repo', async () => {
167 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(true);
168 | 
169 | 			await expect(
170 | 				initializeProject({
171 | 					...baseOptions,
172 | 					git: true,
173 | 					aliases: false,
174 | 					dryRun: false
175 | 				})
176 | 			).resolves.not.toThrow();
177 | 		});
178 | 
179 | 		it('uses default git behavior without errors', async () => {
180 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(false);
181 | 
182 | 			await expect(
183 | 				initializeProject({
184 | 					...baseOptions,
185 | 					aliases: false,
186 | 					dryRun: false
187 | 				})
188 | 			).resolves.not.toThrow();
189 | 		});
190 | 
191 | 		it('handles git command failures gracefully', async () => {
192 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(false);
193 | 			execSync.mockImplementation((cmd) => {
194 | 				if (cmd.includes('git init')) {
195 | 					throw new Error('git not found');
196 | 				}
197 | 				return '';
198 | 			});
199 | 
200 | 			await expect(
201 | 				initializeProject({
202 | 					...baseOptions,
203 | 					git: true,
204 | 					aliases: false,
205 | 					dryRun: false
206 | 				})
207 | 			).resolves.not.toThrow();
208 | 		});
209 | 	});
210 | 
211 | 	describe('Alias Flag Behavior', () => {
212 | 		it('completes successfully when aliases:true and environment is set up', async () => {
213 | 			const originalShell = process.env.SHELL;
214 | 			const originalHome = process.env.HOME;
215 | 
216 | 			process.env.SHELL = '/bin/zsh';
217 | 			process.env.HOME = '/mock/home';
218 | 
219 | 			await expect(
220 | 				initializeProject({
221 | 					...baseOptions,
222 | 					git: false,
223 | 					aliases: true,
224 | 					dryRun: false
225 | 				})
226 | 			).resolves.not.toThrow();
227 | 
228 | 			process.env.SHELL = originalShell;
229 | 			process.env.HOME = originalHome;
230 | 		});
231 | 
232 | 		it('completes successfully when aliases:false', async () => {
233 | 			await expect(
234 | 				initializeProject({
235 | 					...baseOptions,
236 | 					git: false,
237 | 					aliases: false,
238 | 					dryRun: false
239 | 				})
240 | 			).resolves.not.toThrow();
241 | 		});
242 | 
243 | 		it('handles missing shell gracefully', async () => {
244 | 			const originalShell = process.env.SHELL;
245 | 			const originalHome = process.env.HOME;
246 | 
247 | 			delete process.env.SHELL; // Remove shell env var
248 | 			process.env.HOME = '/mock/home';
249 | 
250 | 			await expect(
251 | 				initializeProject({
252 | 					...baseOptions,
253 | 					git: false,
254 | 					aliases: true,
255 | 					dryRun: false
256 | 				})
257 | 			).resolves.not.toThrow();
258 | 
259 | 			process.env.SHELL = originalShell;
260 | 			process.env.HOME = originalHome;
261 | 		});
262 | 
263 | 		it('handles missing shell config file gracefully', async () => {
264 | 			const originalShell = process.env.SHELL;
265 | 			const originalHome = process.env.HOME;
266 | 
267 | 			process.env.SHELL = '/bin/zsh';
268 | 			process.env.HOME = '/mock/home';
269 | 
270 | 			// Shell config doesn't exist
271 | 			mockFs.existsSync.mockImplementation((filePath) => {
272 | 				if (filePath.includes('.zshrc') || filePath.includes('.bashrc')) {
273 | 					return false;
274 | 				}
275 | 				if (filePath.includes('assets') || filePath.includes('.cursor/rules')) {
276 | 					return true;
277 | 				}
278 | 				return false;
279 | 			});
280 | 
281 | 			await expect(
282 | 				initializeProject({
283 | 					...baseOptions,
284 | 					git: false,
285 | 					aliases: true,
286 | 					dryRun: false
287 | 				})
288 | 			).resolves.not.toThrow();
289 | 
290 | 			process.env.SHELL = originalShell;
291 | 			process.env.HOME = originalHome;
292 | 		});
293 | 	});
294 | 
295 | 	describe('Flag Combinations', () => {
296 | 		it.each`
297 | 			git      | aliases  | description
298 | 			${true}  | ${true}  | ${'git & aliases enabled'}
299 | 			${true}  | ${false} | ${'git enabled, aliases disabled'}
300 | 			${false} | ${true}  | ${'git disabled, aliases enabled'}
301 | 			${false} | ${false} | ${'git & aliases disabled'}
302 | 		`('handles $description without errors', async ({ git, aliases }) => {
303 | 			const originalShell = process.env.SHELL;
304 | 			const originalHome = process.env.HOME;
305 | 
306 | 			if (aliases) {
307 | 				process.env.SHELL = '/bin/zsh';
308 | 				process.env.HOME = '/mock/home';
309 | 			}
310 | 
311 | 			if (git) {
312 | 				mockGitUtils.insideGitWorkTree.mockReturnValue(false);
313 | 			}
314 | 
315 | 			await expect(
316 | 				initializeProject({
317 | 					...baseOptions,
318 | 					git,
319 | 					aliases,
320 | 					dryRun: false
321 | 				})
322 | 			).resolves.not.toThrow();
323 | 
324 | 			process.env.SHELL = originalShell;
325 | 			process.env.HOME = originalHome;
326 | 		});
327 | 	});
328 | 
329 | 	describe('Dry Run Mode', () => {
330 | 		it('returns dry run result and performs no operations', async () => {
331 | 			const result = await initializeProject({
332 | 				...baseOptions,
333 | 				git: true,
334 | 				aliases: true,
335 | 				dryRun: true
336 | 			});
337 | 
338 | 			expect(result.dryRun).toBe(true);
339 | 		});
340 | 
341 | 		it.each`
342 | 			git      | aliases  | description
343 | 			${true}  | ${false} | ${'git-specific behavior'}
344 | 			${false} | ${false} | ${'no-git behavior'}
345 | 			${false} | ${true}  | ${'alias behavior'}
346 | 		`('shows $description in dry run', async ({ git, aliases }) => {
347 | 			const result = await initializeProject({
348 | 				...baseOptions,
349 | 				git,
350 | 				aliases,
351 | 				dryRun: true
352 | 			});
353 | 
354 | 			expect(result.dryRun).toBe(true);
355 | 		});
356 | 	});
357 | 
358 | 	describe('Error Handling', () => {
359 | 		it('handles npm install failures gracefully', async () => {
360 | 			execSync.mockImplementation((cmd) => {
361 | 				if (cmd.includes('npm install')) {
362 | 					throw new Error('npm failed');
363 | 				}
364 | 				return '';
365 | 			});
366 | 
367 | 			await expect(
368 | 				initializeProject({
369 | 					...baseOptions,
370 | 					git: false,
371 | 					aliases: false,
372 | 					skipInstall: false,
373 | 					dryRun: false
374 | 				})
375 | 			).resolves.not.toThrow();
376 | 		});
377 | 
378 | 		it('handles git failures gracefully', async () => {
379 | 			mockGitUtils.insideGitWorkTree.mockReturnValue(false);
380 | 			execSync.mockImplementation((cmd) => {
381 | 				if (cmd.includes('git init')) {
382 | 					throw new Error('git failed');
383 | 				}
384 | 				return '';
385 | 			});
386 | 
387 | 			await expect(
388 | 				initializeProject({
389 | 					...baseOptions,
390 | 					git: true,
391 | 					aliases: false,
392 | 					dryRun: false
393 | 				})
394 | 			).resolves.not.toThrow();
395 | 		});
396 | 
397 | 		it('handles file system errors gracefully', async () => {
398 | 			mockFs.mkdirSync.mockImplementation(() => {
399 | 				throw new Error('Permission denied');
400 | 			});
401 | 
402 | 			// Should handle file system errors gracefully
403 | 			await expect(
404 | 				initializeProject({
405 | 					...baseOptions,
406 | 					git: false,
407 | 					aliases: false,
408 | 					dryRun: false
409 | 				})
410 | 			).resolves.not.toThrow();
411 | 		});
412 | 	});
413 | 
414 | 	describe('Non-Interactive Mode', () => {
415 | 		it('bypasses prompts with yes:true', async () => {
416 | 			const result = await initializeProject({
417 | 				...baseOptions,
418 | 				git: true,
419 | 				aliases: true,
420 | 				dryRun: true
421 | 			});
422 | 
423 | 			expect(result).toEqual({ dryRun: true });
424 | 		});
425 | 
426 | 		it('completes without hanging', async () => {
427 | 			await expect(
428 | 				initializeProject({
429 | 					...baseOptions,
430 | 					git: false,
431 | 					aliases: false,
432 | 					dryRun: false
433 | 				})
434 | 			).resolves.not.toThrow();
435 | 		});
436 | 
437 | 		it('handles all flag combinations without hanging', async () => {
438 | 			const flagCombinations = [
439 | 				{ git: true, aliases: true },
440 | 				{ git: true, aliases: false },
441 | 				{ git: false, aliases: true },
442 | 				{ git: false, aliases: false },
443 | 				{} // No flags (uses defaults)
444 | 			];
445 | 
446 | 			for (const flags of flagCombinations) {
447 | 				await expect(
448 | 					initializeProject({
449 | 						...baseOptions,
450 | 						...flags,
451 | 						dryRun: true // Use dry run for speed
452 | 					})
453 | 				).resolves.not.toThrow();
454 | 			}
455 | 		});
456 | 
457 | 		it('accepts complete project details', async () => {
458 | 			await expect(
459 | 				initializeProject({
460 | 					name: 'test-project',
461 | 					description: 'test description',
462 | 					version: '2.0.0',
463 | 					author: 'Test User',
464 | 					git: false,
465 | 					aliases: false,
466 | 					dryRun: true
467 | 				})
468 | 			).resolves.not.toThrow();
469 | 		});
470 | 
471 | 		it('works with skipInstall option', async () => {
472 | 			await expect(
473 | 				initializeProject({
474 | 					...baseOptions,
475 | 					skipInstall: true,
476 | 					git: false,
477 | 					aliases: false,
478 | 					dryRun: false
479 | 				})
480 | 			).resolves.not.toThrow();
481 | 		});
482 | 	});
483 | 
484 | 	describe('Function Integration', () => {
485 | 		it('calls utility functions without errors', async () => {
486 | 			await initializeProject({
487 | 				...baseOptions,
488 | 				git: false,
489 | 				aliases: false,
490 | 				dryRun: false
491 | 			});
492 | 
493 | 			// Verify that utility functions were called
494 | 			expect(mockUtils.isSilentMode).toHaveBeenCalled();
495 | 			expect(
496 | 				mockRuleTransformer.convertAllRulesToProfileRules
497 | 			).toHaveBeenCalled();
498 | 		});
499 | 
500 | 		it('handles template operations gracefully', async () => {
501 | 			// Make file operations throw errors
502 | 			mockFs.writeFileSync.mockImplementation(() => {
503 | 				throw new Error('Write failed');
504 | 			});
505 | 
506 | 			// Should complete despite file operation failures
507 | 			await expect(
508 | 				initializeProject({
509 | 					...baseOptions,
510 | 					git: false,
511 | 					aliases: false,
512 | 					dryRun: false
513 | 				})
514 | 			).resolves.not.toThrow();
515 | 		});
516 | 
517 | 		it('validates boolean flag conversion', async () => {
518 | 			// Test the boolean flag handling specifically
519 | 			await expect(
520 | 				initializeProject({
521 | 					...baseOptions,
522 | 					git: true, // Should convert to initGit: true
523 | 					aliases: false, // Should convert to addAliases: false
524 | 					dryRun: true
525 | 				})
526 | 			).resolves.not.toThrow();
527 | 
528 | 			await expect(
529 | 				initializeProject({
530 | 					...baseOptions,
531 | 					git: false, // Should convert to initGit: false
532 | 					aliases: true, // Should convert to addAliases: true
533 | 					dryRun: true
534 | 				})
535 | 			).resolves.not.toThrow();
536 | 		});
537 | 	});
538 | });
539 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/common/types/database.types.ts:
--------------------------------------------------------------------------------

```typescript
  1 | export type Json =
  2 | 	| string
  3 | 	| number
  4 | 	| boolean
  5 | 	| null
  6 | 	| { [key: string]: Json | undefined }
  7 | 	| Json[];
  8 | 
  9 | export type Database = {
 10 | 	public: {
 11 | 		Tables: {
 12 | 			accounts: {
 13 | 				Row: {
 14 | 					created_at: string | null;
 15 | 					created_by: string | null;
 16 | 					email: string | null;
 17 | 					id: string;
 18 | 					is_personal_account: boolean;
 19 | 					name: string;
 20 | 					picture_url: string | null;
 21 | 					primary_owner_user_id: string;
 22 | 					public_data: Json;
 23 | 					slug: string | null;
 24 | 					updated_at: string | null;
 25 | 					updated_by: string | null;
 26 | 				};
 27 | 				Insert: {
 28 | 					created_at?: string | null;
 29 | 					created_by?: string | null;
 30 | 					email?: string | null;
 31 | 					id?: string;
 32 | 					is_personal_account?: boolean;
 33 | 					name: string;
 34 | 					picture_url?: string | null;
 35 | 					primary_owner_user_id?: string;
 36 | 					public_data?: Json;
 37 | 					slug?: string | null;
 38 | 					updated_at?: string | null;
 39 | 					updated_by?: string | null;
 40 | 				};
 41 | 				Update: {
 42 | 					created_at?: string | null;
 43 | 					created_by?: string | null;
 44 | 					email?: string | null;
 45 | 					id?: string;
 46 | 					is_personal_account?: boolean;
 47 | 					name?: string;
 48 | 					picture_url?: string | null;
 49 | 					primary_owner_user_id?: string;
 50 | 					public_data?: Json;
 51 | 					slug?: string | null;
 52 | 					updated_at?: string | null;
 53 | 					updated_by?: string | null;
 54 | 				};
 55 | 				Relationships: [];
 56 | 			};
 57 | 			brief: {
 58 | 				Row: {
 59 | 					account_id: string;
 60 | 					created_at: string;
 61 | 					created_by: string;
 62 | 					document_id: string;
 63 | 					id: string;
 64 | 					plan_generation_completed_at: string | null;
 65 | 					plan_generation_error: string | null;
 66 | 					plan_generation_started_at: string | null;
 67 | 					plan_generation_status: Database['public']['Enums']['plan_generation_status'];
 68 | 					status: Database['public']['Enums']['brief_status'];
 69 | 					updated_at: string;
 70 | 				};
 71 | 				Insert: {
 72 | 					account_id: string;
 73 | 					created_at?: string;
 74 | 					created_by: string;
 75 | 					document_id: string;
 76 | 					id?: string;
 77 | 					plan_generation_completed_at?: string | null;
 78 | 					plan_generation_error?: string | null;
 79 | 					plan_generation_started_at?: string | null;
 80 | 					plan_generation_status?: Database['public']['Enums']['plan_generation_status'];
 81 | 					status?: Database['public']['Enums']['brief_status'];
 82 | 					updated_at?: string;
 83 | 				};
 84 | 				Update: {
 85 | 					account_id?: string;
 86 | 					created_at?: string;
 87 | 					created_by?: string;
 88 | 					document_id?: string;
 89 | 					id?: string;
 90 | 					plan_generation_completed_at?: string | null;
 91 | 					plan_generation_error?: string | null;
 92 | 					plan_generation_started_at?: string | null;
 93 | 					plan_generation_status?: Database['public']['Enums']['plan_generation_status'];
 94 | 					status?: Database['public']['Enums']['brief_status'];
 95 | 					updated_at?: string;
 96 | 				};
 97 | 				Relationships: [
 98 | 					{
 99 | 						foreignKeyName: 'brief_account_id_fkey';
100 | 						columns: ['account_id'];
101 | 						isOneToOne: false;
102 | 						referencedRelation: 'accounts';
103 | 						referencedColumns: ['id'];
104 | 					},
105 | 					{
106 | 						foreignKeyName: 'brief_document_id_fkey';
107 | 						columns: ['document_id'];
108 | 						isOneToOne: false;
109 | 						referencedRelation: 'document';
110 | 						referencedColumns: ['id'];
111 | 					}
112 | 				];
113 | 			};
114 | 			document: {
115 | 				Row: {
116 | 					account_id: string;
117 | 					created_at: string;
118 | 					created_by: string;
119 | 					description: string | null;
120 | 					document_name: string;
121 | 					document_type: Database['public']['Enums']['document_type'];
122 | 					file_path: string | null;
123 | 					file_size: number | null;
124 | 					id: string;
125 | 					metadata: Json | null;
126 | 					mime_type: string | null;
127 | 					processed_at: string | null;
128 | 					processing_error: string | null;
129 | 					processing_status:
130 | 						| Database['public']['Enums']['document_processing_status']
131 | 						| null;
132 | 					source_id: string | null;
133 | 					source_type: string | null;
134 | 					title: string;
135 | 					updated_at: string;
136 | 				};
137 | 				Insert: {
138 | 					account_id: string;
139 | 					created_at?: string;
140 | 					created_by: string;
141 | 					description?: string | null;
142 | 					document_name: string;
143 | 					document_type?: Database['public']['Enums']['document_type'];
144 | 					file_path?: string | null;
145 | 					file_size?: number | null;
146 | 					id?: string;
147 | 					metadata?: Json | null;
148 | 					mime_type?: string | null;
149 | 					processed_at?: string | null;
150 | 					processing_error?: string | null;
151 | 					processing_status?:
152 | 						| Database['public']['Enums']['document_processing_status']
153 | 						| null;
154 | 					source_id?: string | null;
155 | 					source_type?: string | null;
156 | 					title: string;
157 | 					updated_at?: string;
158 | 				};
159 | 				Update: {
160 | 					account_id?: string;
161 | 					created_at?: string;
162 | 					created_by?: string;
163 | 					description?: string | null;
164 | 					document_name?: string;
165 | 					document_type?: Database['public']['Enums']['document_type'];
166 | 					file_path?: string | null;
167 | 					file_size?: number | null;
168 | 					id?: string;
169 | 					metadata?: Json | null;
170 | 					mime_type?: string | null;
171 | 					processed_at?: string | null;
172 | 					processing_error?: string | null;
173 | 					processing_status?:
174 | 						| Database['public']['Enums']['document_processing_status']
175 | 						| null;
176 | 					source_id?: string | null;
177 | 					source_type?: string | null;
178 | 					title?: string;
179 | 					updated_at?: string;
180 | 				};
181 | 				Relationships: [
182 | 					{
183 | 						foreignKeyName: 'document_account_id_fkey';
184 | 						columns: ['account_id'];
185 | 						isOneToOne: false;
186 | 						referencedRelation: 'accounts';
187 | 						referencedColumns: ['id'];
188 | 					}
189 | 				];
190 | 			};
191 | 			tasks: {
192 | 				Row: {
193 | 					account_id: string;
194 | 					actual_hours: number;
195 | 					assignee_id: string | null;
196 | 					brief_id: string | null;
197 | 					completed_subtasks: number;
198 | 					complexity: number | null;
199 | 					created_at: string;
200 | 					created_by: string;
201 | 					description: string | null;
202 | 					display_id: string | null;
203 | 					document_id: string | null;
204 | 					due_date: string | null;
205 | 					estimated_hours: number | null;
206 | 					id: string;
207 | 					metadata: Json;
208 | 					parent_task_id: string | null;
209 | 					position: number;
210 | 					priority: Database['public']['Enums']['task_priority'];
211 | 					status: Database['public']['Enums']['task_status'];
212 | 					subtask_position: number;
213 | 					title: string;
214 | 					total_subtasks: number;
215 | 					updated_at: string;
216 | 					updated_by: string;
217 | 				};
218 | 				Insert: {
219 | 					account_id: string;
220 | 					actual_hours?: number;
221 | 					assignee_id?: string | null;
222 | 					brief_id?: string | null;
223 | 					completed_subtasks?: number;
224 | 					complexity?: number | null;
225 | 					created_at?: string;
226 | 					created_by: string;
227 | 					description?: string | null;
228 | 					display_id?: string | null;
229 | 					document_id?: string | null;
230 | 					due_date?: string | null;
231 | 					estimated_hours?: number | null;
232 | 					id?: string;
233 | 					metadata?: Json;
234 | 					parent_task_id?: string | null;
235 | 					position?: number;
236 | 					priority?: Database['public']['Enums']['task_priority'];
237 | 					status?: Database['public']['Enums']['task_status'];
238 | 					subtask_position?: number;
239 | 					title: string;
240 | 					total_subtasks?: number;
241 | 					updated_at?: string;
242 | 					updated_by: string;
243 | 				};
244 | 				Update: {
245 | 					account_id?: string;
246 | 					actual_hours?: number;
247 | 					assignee_id?: string | null;
248 | 					brief_id?: string | null;
249 | 					completed_subtasks?: number;
250 | 					complexity?: number | null;
251 | 					created_at?: string;
252 | 					created_by?: string;
253 | 					description?: string | null;
254 | 					display_id?: string | null;
255 | 					document_id?: string | null;
256 | 					due_date?: string | null;
257 | 					estimated_hours?: number | null;
258 | 					id?: string;
259 | 					metadata?: Json;
260 | 					parent_task_id?: string | null;
261 | 					position?: number;
262 | 					priority?: Database['public']['Enums']['task_priority'];
263 | 					status?: Database['public']['Enums']['task_status'];
264 | 					subtask_position?: number;
265 | 					title?: string;
266 | 					total_subtasks?: number;
267 | 					updated_at?: string;
268 | 					updated_by?: string;
269 | 				};
270 | 				Relationships: [
271 | 					{
272 | 						foreignKeyName: 'tasks_account_id_fkey';
273 | 						columns: ['account_id'];
274 | 						isOneToOne: false;
275 | 						referencedRelation: 'accounts';
276 | 						referencedColumns: ['id'];
277 | 					},
278 | 					{
279 | 						foreignKeyName: 'tasks_brief_id_fkey';
280 | 						columns: ['brief_id'];
281 | 						isOneToOne: false;
282 | 						referencedRelation: 'brief';
283 | 						referencedColumns: ['id'];
284 | 					},
285 | 					{
286 | 						foreignKeyName: 'tasks_document_id_fkey';
287 | 						columns: ['document_id'];
288 | 						isOneToOne: false;
289 | 						referencedRelation: 'document';
290 | 						referencedColumns: ['id'];
291 | 					},
292 | 					{
293 | 						foreignKeyName: 'tasks_parent_task_id_fkey';
294 | 						columns: ['parent_task_id'];
295 | 						isOneToOne: false;
296 | 						referencedRelation: 'tasks';
297 | 						referencedColumns: ['id'];
298 | 					}
299 | 				];
300 | 			};
301 | 			task_dependencies: {
302 | 				Row: {
303 | 					account_id: string;
304 | 					created_at: string;
305 | 					depends_on_task_id: string;
306 | 					id: string;
307 | 					task_id: string;
308 | 				};
309 | 				Insert: {
310 | 					account_id: string;
311 | 					created_at?: string;
312 | 					depends_on_task_id: string;
313 | 					id?: string;
314 | 					task_id: string;
315 | 				};
316 | 				Update: {
317 | 					account_id?: string;
318 | 					created_at?: string;
319 | 					depends_on_task_id?: string;
320 | 					id?: string;
321 | 					task_id?: string;
322 | 				};
323 | 				Relationships: [
324 | 					{
325 | 						foreignKeyName: 'task_dependencies_account_id_fkey';
326 | 						columns: ['account_id'];
327 | 						isOneToOne: false;
328 | 						referencedRelation: 'accounts';
329 | 						referencedColumns: ['id'];
330 | 					},
331 | 					{
332 | 						foreignKeyName: 'task_dependencies_depends_on_task_id_fkey';
333 | 						columns: ['depends_on_task_id'];
334 | 						isOneToOne: false;
335 | 						referencedRelation: 'tasks';
336 | 						referencedColumns: ['id'];
337 | 					},
338 | 					{
339 | 						foreignKeyName: 'task_dependencies_task_id_fkey';
340 | 						columns: ['task_id'];
341 | 						isOneToOne: false;
342 | 						referencedRelation: 'tasks';
343 | 						referencedColumns: ['id'];
344 | 					}
345 | 				];
346 | 			};
347 | 			user_accounts: {
348 | 				Row: {
349 | 					id: string | null;
350 | 					name: string | null;
351 | 					picture_url: string | null;
352 | 					role: string | null;
353 | 					slug: string | null;
354 | 				};
355 | 				Insert: {
356 | 					id?: string | null;
357 | 					name?: string | null;
358 | 					picture_url?: string | null;
359 | 					role?: string | null;
360 | 					slug?: string | null;
361 | 				};
362 | 				Update: {
363 | 					id?: string | null;
364 | 					name?: string | null;
365 | 					picture_url?: string | null;
366 | 					role?: string | null;
367 | 					slug?: string | null;
368 | 				};
369 | 				Relationships: [];
370 | 			};
371 | 		};
372 | 		Views: {
373 | 			[_ in never]: never;
374 | 		};
375 | 		Functions: {
376 | 			[_ in never]: never;
377 | 		};
378 | 		Enums: {
379 | 			brief_status:
380 | 				| 'draft'
381 | 				| 'refining'
382 | 				| 'aligned'
383 | 				| 'delivering'
384 | 				| 'delivered'
385 | 				| 'done'
386 | 				| 'archived';
387 | 			document_processing_status: 'pending' | 'processing' | 'ready' | 'failed';
388 | 			document_type:
389 | 				| 'brief'
390 | 				| 'blueprint'
391 | 				| 'file'
392 | 				| 'note'
393 | 				| 'transcript'
394 | 				| 'generated_plan'
395 | 				| 'generated_task'
396 | 				| 'generated_summary'
397 | 				| 'method'
398 | 				| 'task';
399 | 			plan_generation_status:
400 | 				| 'not_started'
401 | 				| 'generating'
402 | 				| 'completed'
403 | 				| 'failed';
404 | 			task_priority: 'low' | 'medium' | 'high' | 'urgent';
405 | 			task_status: 'todo' | 'in_progress' | 'done';
406 | 		};
407 | 		CompositeTypes: {
408 | 			[_ in never]: never;
409 | 		};
410 | 	};
411 | };
412 | 
413 | export type Tables<
414 | 	PublicTableNameOrOptions extends
415 | 		| keyof (Database['public']['Tables'] & Database['public']['Views'])
416 | 		| { schema: keyof Database },
417 | 	TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
418 | 		? keyof (Database[PublicTableNameOrOptions['schema']]['Tables'] &
419 | 				Database[PublicTableNameOrOptions['schema']]['Views'])
420 | 		: never = never
421 | > = PublicTableNameOrOptions extends { schema: keyof Database }
422 | 	? (Database[PublicTableNameOrOptions['schema']]['Tables'] &
423 | 			Database[PublicTableNameOrOptions['schema']]['Views'])[TableName] extends {
424 | 			Row: infer R;
425 | 		}
426 | 		? R
427 | 		: never
428 | 	: PublicTableNameOrOptions extends keyof (Database['public']['Tables'] &
429 | 				Database['public']['Views'])
430 | 		? (Database['public']['Tables'] &
431 | 				Database['public']['Views'])[PublicTableNameOrOptions] extends {
432 | 				Row: infer R;
433 | 			}
434 | 			? R
435 | 			: never
436 | 		: never;
437 | 
438 | export type TablesInsert<
439 | 	PublicTableNameOrOptions extends
440 | 		| keyof Database['public']['Tables']
441 | 		| { schema: keyof Database },
442 | 	TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
443 | 		? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
444 | 		: never = never
445 | > = PublicTableNameOrOptions extends { schema: keyof Database }
446 | 	? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends {
447 | 			Insert: infer I;
448 | 		}
449 | 		? I
450 | 		: never
451 | 	: PublicTableNameOrOptions extends keyof Database['public']['Tables']
452 | 		? Database['public']['Tables'][PublicTableNameOrOptions] extends {
453 | 				Insert: infer I;
454 | 			}
455 | 			? I
456 | 			: never
457 | 		: never;
458 | 
459 | export type TablesUpdate<
460 | 	PublicTableNameOrOptions extends
461 | 		| keyof Database['public']['Tables']
462 | 		| { schema: keyof Database },
463 | 	TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
464 | 		? keyof Database[PublicTableNameOrOptions['schema']]['Tables']
465 | 		: never = never
466 | > = PublicTableNameOrOptions extends { schema: keyof Database }
467 | 	? Database[PublicTableNameOrOptions['schema']]['Tables'][TableName] extends {
468 | 			Update: infer U;
469 | 		}
470 | 		? U
471 | 		: never
472 | 	: PublicTableNameOrOptions extends keyof Database['public']['Tables']
473 | 		? Database['public']['Tables'][PublicTableNameOrOptions] extends {
474 | 				Update: infer U;
475 | 			}
476 | 			? U
477 | 			: never
478 | 		: never;
479 | 
480 | export type Enums<
481 | 	PublicEnumNameOrOptions extends
482 | 		| keyof Database['public']['Enums']
483 | 		| { schema: keyof Database },
484 | 	EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
485 | 		? keyof Database[PublicEnumNameOrOptions['schema']]['Enums']
486 | 		: never = never
487 | > = PublicEnumNameOrOptions extends { schema: keyof Database }
488 | 	? Database[PublicEnumNameOrOptions['schema']]['Enums'][EnumName]
489 | 	: PublicEnumNameOrOptions extends keyof Database['public']['Enums']
490 | 		? Database['public']['Enums'][PublicEnumNameOrOptions]
491 | 		: never;
492 | 
```
Page 37/69FirstPrevNextLast