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 |
```