This is page 16 of 50. Use http://codebase.md/eyaltoledano/claude-task-master?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
--------------------------------------------------------------------------------
/tests/unit/ui.test.js:
--------------------------------------------------------------------------------
```javascript
/**
* UI module tests
*/
import { jest } from '@jest/globals';
import {
getStatusWithColor,
formatDependenciesWithStatus,
createProgressBar,
getComplexityWithColor
} from '../../scripts/modules/ui.js';
import { sampleTasks } from '../fixtures/sample-tasks.js';
// Mock dependencies
jest.mock('chalk', () => {
const origChalkFn = (text) => text;
const chalk = origChalkFn;
chalk.green = (text) => text; // Return text as-is for status functions
chalk.yellow = (text) => text;
chalk.red = (text) => text;
chalk.cyan = (text) => text;
chalk.blue = (text) => text;
chalk.gray = (text) => text;
chalk.white = (text) => text;
chalk.bold = (text) => text;
chalk.dim = (text) => text;
// Add hex and other methods
chalk.hex = () => origChalkFn;
chalk.rgb = () => origChalkFn;
return chalk;
});
jest.mock('figlet', () => ({
textSync: jest.fn(() => 'Task Master Banner')
}));
jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`));
jest.mock('ora', () =>
jest.fn(() => ({
start: jest.fn(),
succeed: jest.fn(),
fail: jest.fn(),
stop: jest.fn()
}))
);
jest.mock('cli-table3', () =>
jest.fn().mockImplementation(() => ({
push: jest.fn(),
toString: jest.fn(() => 'Table Content')
}))
);
jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
jest.mock('../../scripts/modules/utils.js', () => ({
CONFIG: {
projectName: 'Test Project',
projectVersion: '1.0.0'
},
log: jest.fn(),
findTaskById: jest.fn(),
readJSON: jest.fn(),
readComplexityReport: jest.fn(),
truncate: jest.fn((text) => text)
}));
jest.mock('../../scripts/modules/task-manager.js', () => ({
findNextTask: jest.fn(),
analyzeTaskComplexity: jest.fn()
}));
describe('UI Module', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('getStatusWithColor function', () => {
test('should return done status with emoji for console output', () => {
const result = getStatusWithColor('done');
expect(result).toMatch(/done/);
expect(result).toContain('✓');
});
test('should return pending status with emoji for console output', () => {
const result = getStatusWithColor('pending');
expect(result).toMatch(/pending/);
expect(result).toContain('○');
});
test('should return deferred status with emoji for console output', () => {
const result = getStatusWithColor('deferred');
expect(result).toMatch(/deferred/);
expect(result).toContain('x');
});
test('should return in-progress status with emoji for console output', () => {
const result = getStatusWithColor('in-progress');
expect(result).toMatch(/in-progress/);
expect(result).toContain('🔄');
});
test('should return unknown status with emoji for console output', () => {
const result = getStatusWithColor('unknown');
expect(result).toMatch(/unknown/);
expect(result).toContain('❌');
});
test('should use simple icons when forTable is true', () => {
const doneResult = getStatusWithColor('done', true);
expect(doneResult).toMatch(/done/);
expect(doneResult).toContain('✓');
const pendingResult = getStatusWithColor('pending', true);
expect(pendingResult).toMatch(/pending/);
expect(pendingResult).toContain('○');
const inProgressResult = getStatusWithColor('in-progress', true);
expect(inProgressResult).toMatch(/in-progress/);
expect(inProgressResult).toContain('►');
const deferredResult = getStatusWithColor('deferred', true);
expect(deferredResult).toMatch(/deferred/);
expect(deferredResult).toContain('x');
});
});
describe('formatDependenciesWithStatus function', () => {
test('should format dependencies as plain IDs when forConsole is false (default)', () => {
const dependencies = [1, 2, 3];
const allTasks = [
{ id: 1, status: 'done' },
{ id: 2, status: 'pending' },
{ id: 3, status: 'deferred' }
];
const result = formatDependenciesWithStatus(dependencies, allTasks);
// With recent changes, we expect just plain IDs when forConsole is false
expect(result).toBe('1, 2, 3');
});
test('should format dependencies with status indicators when forConsole is true', () => {
const dependencies = [1, 2, 3];
const allTasks = [
{ id: 1, status: 'done' },
{ id: 2, status: 'pending' },
{ id: 3, status: 'deferred' }
];
const result = formatDependenciesWithStatus(dependencies, allTasks, true);
// We can't test for exact color formatting due to our chalk mocks
// Instead, test that the result contains all the expected IDs
expect(result).toContain('1');
expect(result).toContain('2');
expect(result).toContain('3');
// Test that it's a comma-separated list
expect(result.split(', ').length).toBe(3);
});
test('should return "None" for empty dependencies', () => {
const result = formatDependenciesWithStatus([], []);
expect(result).toBe('None');
});
test('should handle missing tasks in the task list', () => {
const dependencies = [1, 999];
const allTasks = [{ id: 1, status: 'done' }];
const result = formatDependenciesWithStatus(dependencies, allTasks);
expect(result).toBe('1, 999 (Not found)');
});
});
describe('createProgressBar function', () => {
test('should create a progress bar with the correct percentage', () => {
const result = createProgressBar(50, 10, {
pending: 20,
'in-progress': 15,
blocked: 5
});
expect(result).toContain('50%');
});
test('should handle 0% progress', () => {
const result = createProgressBar(0, 10);
expect(result).toContain('0%');
});
test('should handle 100% progress', () => {
const result = createProgressBar(100, 10);
expect(result).toContain('100%');
});
test('should handle invalid percentages by clamping', () => {
const result1 = createProgressBar(0, 10);
expect(result1).toContain('0%');
const result2 = createProgressBar(100, 10);
expect(result2).toContain('100%');
});
test('should support status breakdown in the progress bar', () => {
const result = createProgressBar(30, 10, {
pending: 30,
'in-progress': 20,
blocked: 10,
deferred: 5,
cancelled: 5
});
expect(result).toContain('40%');
});
});
describe('getComplexityWithColor function', () => {
test('should return high complexity in red', () => {
const result = getComplexityWithColor(8);
expect(result).toMatch(/8/);
expect(result).toContain('●');
});
test('should return medium complexity in yellow', () => {
const result = getComplexityWithColor(5);
expect(result).toMatch(/5/);
expect(result).toContain('●');
});
test('should return low complexity in green', () => {
const result = getComplexityWithColor(3);
expect(result).toMatch(/3/);
expect(result).toContain('●');
});
test('should handle non-numeric inputs', () => {
const result = getComplexityWithColor('high');
expect(result).toMatch(/high/);
expect(result).toContain('●');
});
});
});
```
--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/research.js:
--------------------------------------------------------------------------------
```javascript
/**
* research.js
* Direct function implementation for AI-powered research queries
*/
import path from 'path';
import { performResearch } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for performing AI-powered research with project context.
*
* @param {Object} args - Command arguments
* @param {string} args.query - Research query/prompt (required)
* @param {string} [args.taskIds] - Comma-separated list of task/subtask IDs for context
* @param {string} [args.filePaths] - Comma-separated list of file paths for context
* @param {string} [args.customContext] - Additional custom context text
* @param {boolean} [args.includeProjectTree=false] - Include project file tree in context
* @param {string} [args.detailLevel='medium'] - Detail level: 'low', 'medium', 'high'
* @param {string} [args.saveTo] - Automatically save to task/subtask ID (e.g., "15" or "15.2")
* @param {boolean} [args.saveToFile=false] - Save research results to .taskmaster/docs/research/ directory
* @param {string} [args.projectRoot] - Project root path
* @param {string} [args.tag] - Tag for the task (optional)
* @param {Object} log - Logger object
* @param {Object} context - Additional context (session)
* @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function researchDirect(args, log, context = {}) {
// Destructure expected args
const {
query,
taskIds,
filePaths,
customContext,
includeProjectTree = false,
detailLevel = 'medium',
saveTo,
saveToFile = false,
projectRoot,
tag
} = args;
const { session } = context; // Destructure session from context
// Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode();
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
try {
// Check required parameters
if (!query || typeof query !== 'string' || query.trim().length === 0) {
log.error('Missing or invalid required parameter: query');
disableSilentMode();
return {
success: false,
error: {
code: 'MISSING_PARAMETER',
message:
'The query parameter is required and must be a non-empty string'
}
};
}
// Parse comma-separated task IDs if provided
const parsedTaskIds = taskIds
? taskIds
.split(',')
.map((id) => id.trim())
.filter((id) => id.length > 0)
: [];
// Parse comma-separated file paths if provided
const parsedFilePaths = filePaths
? filePaths
.split(',')
.map((path) => path.trim())
.filter((path) => path.length > 0)
: [];
// Validate detail level
const validDetailLevels = ['low', 'medium', 'high'];
if (!validDetailLevels.includes(detailLevel)) {
log.error(`Invalid detail level: ${detailLevel}`);
disableSilentMode();
return {
success: false,
error: {
code: 'INVALID_PARAMETER',
message: `Detail level must be one of: ${validDetailLevels.join(', ')}`
}
};
}
log.info(
`Performing research query: "${query.substring(0, 100)}${query.length > 100 ? '...' : ''}", ` +
`taskIds: [${parsedTaskIds.join(', ')}], ` +
`filePaths: [${parsedFilePaths.join(', ')}], ` +
`detailLevel: ${detailLevel}, ` +
`includeProjectTree: ${includeProjectTree}, ` +
`projectRoot: ${projectRoot}`
);
// Prepare options for the research function
const researchOptions = {
taskIds: parsedTaskIds,
filePaths: parsedFilePaths,
customContext: customContext || '',
includeProjectTree,
detailLevel,
projectRoot,
tag,
saveToFile
};
// Prepare context for the research function
const researchContext = {
session,
mcpLog,
commandName: 'research',
outputType: 'mcp'
};
// Call the performResearch function
const result = await performResearch(
query.trim(),
researchOptions,
researchContext,
'json', // outputFormat - use 'json' to suppress CLI UI
false // allowFollowUp - disable for MCP calls
);
// Auto-save to task/subtask if requested
if (saveTo) {
try {
const isSubtask = saveTo.includes('.');
// Format research content for saving
const researchContent = `## Research Query: ${query.trim()}
**Detail Level:** ${result.detailLevel}
**Context Size:** ${result.contextSize} characters
**Timestamp:** ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}
### Results
${result.result}`;
if (isSubtask) {
// Save to subtask
const { updateSubtaskById } = await import(
'../../../../scripts/modules/task-manager/update-subtask-by-id.js'
);
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
await updateSubtaskById(
tasksPath,
saveTo,
researchContent,
false, // useResearch = false for simple append
{
session,
mcpLog,
commandName: 'research-save',
outputType: 'mcp',
projectRoot,
tag
},
'json'
);
log.info(`Research saved to subtask ${saveTo}`);
} else {
// Save to task
const updateTaskById = (
await import(
'../../../../scripts/modules/task-manager/update-task-by-id.js'
)
).default;
const taskIdNum = parseInt(saveTo, 10);
const tasksPath = path.join(
projectRoot,
'.taskmaster',
'tasks',
'tasks.json'
);
await updateTaskById(
tasksPath,
taskIdNum,
researchContent,
false, // useResearch = false for simple append
{
session,
mcpLog,
commandName: 'research-save',
outputType: 'mcp',
projectRoot,
tag
},
'json',
true // appendMode = true
);
log.info(`Research saved to task ${saveTo}`);
}
} catch (saveError) {
log.warn(`Error saving research to task/subtask: ${saveError.message}`);
}
}
// Restore normal logging
disableSilentMode();
return {
success: true,
data: {
query: result.query,
result: result.result,
contextSize: result.contextSize,
contextTokens: result.contextTokens,
tokenBreakdown: result.tokenBreakdown,
systemPromptTokens: result.systemPromptTokens,
userPromptTokens: result.userPromptTokens,
totalInputTokens: result.totalInputTokens,
detailLevel: result.detailLevel,
telemetryData: result.telemetryData,
tagInfo: result.tagInfo,
savedFilePath: result.savedFilePath
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
disableSilentMode();
log.error(`Error in researchDirect: ${error.message}`);
return {
success: false,
error: {
code: error.code || 'RESEARCH_ERROR',
message: error.message
}
};
}
}
```
--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/analyze-task-complexity.js:
--------------------------------------------------------------------------------
```javascript
/**
* Direct function wrapper for analyzeTaskComplexity
*/
import analyzeTaskComplexity from '../../../../scripts/modules/task-manager/analyze-task-complexity.js';
import {
enableSilentMode,
disableSilentMode,
isSilentMode
} from '../../../../scripts/modules/utils.js';
import fs from 'fs';
import { createLogWrapper } from '../../tools/utils.js'; // Import the new utility
/**
* Analyze task complexity and generate recommendations
* @param {Object} args - Function arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.outputPath - Explicit absolute path to save the report.
* @param {string|number} [args.threshold] - Minimum complexity score to recommend expansion (1-10)
* @param {boolean} [args.research] - Use Perplexity AI for research-backed complexity analysis
* @param {string} [args.ids] - Comma-separated list of task IDs to analyze
* @param {number} [args.from] - Starting task ID in a range to analyze
* @param {number} [args.to] - Ending task ID in a range to analyze
* @param {string} [args.projectRoot] - Project root path.
* @param {string} [args.tag] - Tag for the task (optional)
* @param {Object} log - Logger object
* @param {Object} [context={}] - Context object containing session data
* @param {Object} [context.session] - MCP session object
* @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
*/
export async function analyzeTaskComplexityDirect(args, log, context = {}) {
const { session } = context;
const {
tasksJsonPath,
outputPath,
threshold,
research,
projectRoot,
ids,
from,
to,
tag
} = args;
const logWrapper = createLogWrapper(log);
// --- Initial Checks (remain the same) ---
try {
log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
if (!tasksJsonPath) {
log.error('analyzeTaskComplexityDirect called without tasksJsonPath');
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
if (!outputPath) {
log.error('analyzeTaskComplexityDirect called without outputPath');
return {
success: false,
error: { code: 'MISSING_ARGUMENT', message: 'outputPath is required' }
};
}
const tasksPath = tasksJsonPath;
const resolvedOutputPath = outputPath;
log.info(`Analyzing task complexity from: ${tasksPath}`);
log.info(`Output report will be saved to: ${resolvedOutputPath}`);
if (ids) {
log.info(`Analyzing specific task IDs: ${ids}`);
} else if (from || to) {
const fromStr = from !== undefined ? from : 'first';
const toStr = to !== undefined ? to : 'last';
log.info(`Analyzing tasks in range: ${fromStr} to ${toStr}`);
}
if (research) {
log.info('Using research role for complexity analysis');
}
// Prepare options for the core function - REMOVED mcpLog and session here
const coreOptions = {
file: tasksJsonPath,
output: outputPath,
threshold: threshold,
research: research === true, // Ensure boolean
projectRoot: projectRoot, // Pass projectRoot here
id: ids, // Pass the ids parameter to the core function as 'id'
from: from, // Pass from parameter
to: to, // Pass to parameter
tag // forward tag
};
// --- End Initial Checks ---
// --- Silent Mode and Logger Wrapper ---
const wasSilent = isSilentMode();
if (!wasSilent) {
enableSilentMode(); // Still enable silent mode as a backup
}
let report;
let coreResult;
try {
// --- Call Core Function (Pass context separately) ---
// Pass coreOptions as the first argument
// Pass context object { session, mcpLog } as the second argument
coreResult = await analyzeTaskComplexity(coreOptions, {
session,
mcpLog: logWrapper,
commandName: 'analyze-complexity',
outputType: 'mcp',
projectRoot,
tag
});
report = coreResult.report;
} catch (error) {
log.error(
`Error in analyzeTaskComplexity core function: ${error.message}`
);
// Restore logging if we changed it
if (!wasSilent && isSilentMode()) {
disableSilentMode();
}
return {
success: false,
error: {
code: 'ANALYZE_CORE_ERROR',
message: `Error running core complexity analysis: ${error.message}`
}
};
} finally {
// Always restore normal logging in finally block if we enabled silent mode
if (!wasSilent && isSilentMode()) {
disableSilentMode();
}
}
// --- Result Handling (remains largely the same) ---
// Verify the report file was created (core function writes it)
if (!fs.existsSync(resolvedOutputPath)) {
return {
success: false,
error: {
code: 'ANALYZE_REPORT_MISSING', // Specific code
message:
'Analysis completed but no report file was created at the expected path.'
}
};
}
if (
!coreResult ||
!coreResult.report ||
typeof coreResult.report !== 'object'
) {
log.error(
'Core analysis function returned an invalid or undefined response.'
);
return {
success: false,
error: {
code: 'INVALID_CORE_RESPONSE',
message: 'Core analysis function returned an invalid response.'
}
};
}
try {
// Ensure complexityAnalysis exists and is an array
const analysisArray = Array.isArray(coreResult.report.complexityAnalysis)
? coreResult.report.complexityAnalysis
: [];
// Count tasks by complexity (remains the same)
const highComplexityTasks = analysisArray.filter(
(t) => t.complexityScore >= 8
).length;
const mediumComplexityTasks = analysisArray.filter(
(t) => t.complexityScore >= 5 && t.complexityScore < 8
).length;
const lowComplexityTasks = analysisArray.filter(
(t) => t.complexityScore < 5
).length;
return {
success: true,
data: {
message: `Task complexity analysis complete. Report saved to ${outputPath}`,
reportPath: outputPath,
reportSummary: {
taskCount: analysisArray.length,
highComplexityTasks,
mediumComplexityTasks,
lowComplexityTasks
},
fullReport: coreResult.report,
telemetryData: coreResult.telemetryData,
tagInfo: coreResult.tagInfo
}
};
} catch (parseError) {
// Should not happen if core function returns object, but good safety check
log.error(`Internal error processing report data: ${parseError.message}`);
return {
success: false,
error: {
code: 'REPORT_PROCESS_ERROR',
message: `Internal error processing complexity report: ${parseError.message}`
}
};
}
// --- End Result Handling ---
} catch (error) {
// Catch errors from initial checks or path resolution
// Make sure to restore normal logging if silent mode was enabled
if (isSilentMode()) {
disableSilentMode();
}
log.error(`Error in analyzeTaskComplexityDirect setup: ${error.message}`);
return {
success: false,
error: {
code: 'DIRECT_FUNCTION_SETUP_ERROR',
message: error.message
}
};
}
}
```
--------------------------------------------------------------------------------
/tests/unit/ai-providers/claude-code.test.js:
--------------------------------------------------------------------------------
```javascript
import { jest } from '@jest/globals';
// Mock the ai-sdk-provider-claude-code package
jest.unstable_mockModule('ai-sdk-provider-claude-code', () => ({
createClaudeCode: jest.fn(() => {
const provider = (modelId, settings) => ({
// Minimal mock language model surface
id: modelId,
settings,
doGenerate: jest.fn(() => ({ text: 'ok', usage: {} })),
doStream: jest.fn(() => ({ stream: true }))
});
provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
provider.chat = provider.languageModel;
return provider;
})
}));
// Mock the base provider
jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
BaseAIProvider: class {
constructor() {
this.name = 'Base Provider';
}
handleError(context, error) {
throw error;
}
}
}));
// Mock config getters
jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
getClaudeCodeSettingsForCommand: jest.fn(() => ({})),
getSupportedModelsForProvider: jest.fn(() => ['opus', 'sonnet', 'haiku']),
getDebugFlag: jest.fn(() => false),
getLogLevel: jest.fn(() => 'info')
}));
// Import after mocking
const { ClaudeCodeProvider } = await import(
'../../../src/ai-providers/claude-code.js'
);
describe('ClaudeCodeProvider', () => {
let provider;
beforeEach(() => {
provider = new ClaudeCodeProvider();
jest.clearAllMocks();
});
describe('constructor', () => {
it('should set the provider name to Claude Code', () => {
expect(provider.name).toBe('Claude Code');
});
});
describe('validateAuth', () => {
it('should not throw an error (no API key required)', () => {
expect(() => provider.validateAuth({})).not.toThrow();
});
it('should not require any parameters', () => {
expect(() => provider.validateAuth()).not.toThrow();
});
it('should work with any params passed', () => {
expect(() =>
provider.validateAuth({
apiKey: 'some-key',
baseURL: 'https://example.com'
})
).not.toThrow();
});
});
describe('getClient', () => {
it('should return a claude code client', () => {
const client = provider.getClient({});
expect(client).toBeDefined();
expect(typeof client).toBe('function');
});
it('should create client without parameters', () => {
const client = provider.getClient();
expect(client).toBeDefined();
});
it('should handle commandName parameter', () => {
const client = provider.getClient({
commandName: 'test-command'
});
expect(client).toBeDefined();
});
it('should have languageModel and chat methods', () => {
const client = provider.getClient({});
expect(client.languageModel).toBeDefined();
expect(client.chat).toBeDefined();
expect(client.chat).toBe(client.languageModel);
});
it('should pass systemPrompt configuration to createClaudeCode', async () => {
const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
provider.getClient({});
expect(createClaudeCode).toHaveBeenCalledWith(
expect.objectContaining({
defaultSettings: expect.objectContaining({
systemPrompt: {
type: 'preset',
preset: 'claude_code'
}
})
})
);
});
it('should pass settingSources configuration to createClaudeCode', async () => {
const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
provider.getClient({});
expect(createClaudeCode).toHaveBeenCalledWith(
expect.objectContaining({
defaultSettings: expect.objectContaining({
settingSources: ['user', 'project', 'local']
})
})
);
});
it('should pass defaultSettings from config to createClaudeCode', async () => {
const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
const { getClaudeCodeSettingsForCommand } = await import(
'../../../scripts/modules/config-manager.js'
);
const mockSettings = { maxTokens: 4096, temperature: 0.7 };
getClaudeCodeSettingsForCommand.mockReturnValueOnce(mockSettings);
provider.getClient({ commandName: 'test-command' });
expect(createClaudeCode).toHaveBeenCalledWith(
expect.objectContaining({
defaultSettings: expect.objectContaining({
...mockSettings,
systemPrompt: {
type: 'preset',
preset: 'claude_code'
},
settingSources: ['user', 'project', 'local']
})
})
);
});
it('should pass complete configuration object to createClaudeCode', async () => {
const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
const { getClaudeCodeSettingsForCommand } = await import(
'../../../scripts/modules/config-manager.js'
);
const mockSettings = { maxTokens: 2048 };
getClaudeCodeSettingsForCommand.mockReturnValueOnce(mockSettings);
provider.getClient({ commandName: 'analyze' });
// Verify the complete configuration structure matches v2.0 migration requirements
expect(createClaudeCode).toHaveBeenCalledWith({
defaultSettings: {
...mockSettings,
// Restores pre-2.0 behavior: explicit system prompt preset
systemPrompt: {
type: 'preset',
preset: 'claude_code'
},
// Restores pre-2.0 behavior: enables loading of CLAUDE.md and settings.json
settingSources: ['user', 'project', 'local']
}
});
});
it('should pass empty defaultSettings when config returns null', async () => {
const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
const { getClaudeCodeSettingsForCommand } = await import(
'../../../scripts/modules/config-manager.js'
);
getClaudeCodeSettingsForCommand.mockReturnValueOnce(null);
provider.getClient({});
expect(createClaudeCode).toHaveBeenCalledWith(
expect.objectContaining({
defaultSettings: expect.objectContaining({
systemPrompt: {
type: 'preset',
preset: 'claude_code'
},
settingSources: ['user', 'project', 'local']
})
})
);
});
});
describe('model support', () => {
it('should return supported models', () => {
const models = provider.getSupportedModels();
expect(models).toEqual(['opus', 'sonnet', 'haiku']);
});
it('should check if model is supported', () => {
expect(provider.isModelSupported('sonnet')).toBe(true);
expect(provider.isModelSupported('opus')).toBe(true);
expect(provider.isModelSupported('haiku')).toBe(true);
expect(provider.isModelSupported('unknown')).toBe(false);
});
});
describe('error handling', () => {
it('should handle client initialization errors', async () => {
// Force an error by making createClaudeCode throw
const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
createClaudeCode.mockImplementationOnce(() => {
throw new Error('Mock initialization error');
});
// Create a new provider instance to use the mocked createClaudeCode
const errorProvider = new ClaudeCodeProvider();
expect(() => errorProvider.getClient({})).toThrow(
'Mock initialization error'
);
});
});
});
```
--------------------------------------------------------------------------------
/tests/unit/task-manager/tag-boundary.test.js:
--------------------------------------------------------------------------------
```javascript
import fs from 'fs';
import path from 'path';
import {
createTag,
useTag,
deleteTag
} from '../../../scripts/modules/task-manager/tag-management.js';
// Temporary workspace for each test run
const TEMP_DIR = path.join(process.cwd(), '.tmp_tag_boundary');
const TASKS_PATH = path.join(TEMP_DIR, 'tasks.json');
const STATE_PATH = path.join(TEMP_DIR, '.taskmaster', 'state.json');
function seedWorkspace() {
// Reset temp dir
fs.rmSync(TEMP_DIR, { recursive: true, force: true });
fs.mkdirSync(path.join(TEMP_DIR, '.taskmaster'), {
recursive: true,
force: true
});
// Minimal master tag file
fs.writeFileSync(
TASKS_PATH,
JSON.stringify(
{
master: {
tasks: [{ id: 1, title: 'Seed task', status: 'pending' }],
metadata: { created: new Date().toISOString() }
}
},
null,
2
),
'utf8'
);
// Initial state.json
fs.writeFileSync(
STATE_PATH,
JSON.stringify(
{ currentTag: 'master', lastSwitched: new Date().toISOString() },
null,
2
),
'utf8'
);
}
describe('Tag boundary resolution', () => {
beforeEach(seedWorkspace);
afterAll(() => fs.rmSync(TEMP_DIR, { recursive: true, force: true }));
it('switches currentTag in state.json when useTag succeeds', async () => {
await createTag(
TASKS_PATH,
'feature-x',
{},
{ projectRoot: TEMP_DIR },
'json'
);
await useTag(
TASKS_PATH,
'feature-x',
{},
{ projectRoot: TEMP_DIR },
'json'
);
const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
expect(state.currentTag).toBe('feature-x');
});
it('throws error when switching to non-existent tag', async () => {
await expect(
useTag(TASKS_PATH, 'ghost', {}, { projectRoot: TEMP_DIR }, 'json')
).rejects.toThrow(/does not exist/);
});
it('deleting active tag auto-switches back to master', async () => {
await createTag(TASKS_PATH, 'temp', {}, { projectRoot: TEMP_DIR }, 'json');
await useTag(TASKS_PATH, 'temp', {}, { projectRoot: TEMP_DIR }, 'json');
// Delete the active tag with force flag (yes: true)
await deleteTag(
TASKS_PATH,
'temp',
{ yes: true },
{ projectRoot: TEMP_DIR },
'json'
);
const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
expect(state.currentTag).toBe('master');
const tasksFile = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
expect(tasksFile.temp).toBeUndefined();
expect(tasksFile.master).toBeDefined();
});
it('createTag with copyFromCurrent deep-copies tasks (mutation isolated)', async () => {
// create new tag with copy
await createTag(
TASKS_PATH,
'alpha',
{ copyFromCurrent: true },
{ projectRoot: TEMP_DIR },
'json'
);
// mutate a field inside alpha tasks
const updatedData = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
updatedData.alpha.tasks[0].title = 'Changed in alpha';
fs.writeFileSync(TASKS_PATH, JSON.stringify(updatedData, null, 2));
const finalData = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
expect(finalData.master.tasks[0].title).toBe('Seed task');
expect(finalData.alpha.tasks[0].title).toBe('Changed in alpha');
});
it('addTask to non-master tag does not leak into master', async () => {
// create and switch
await createTag(
TASKS_PATH,
'feature-api',
{},
{ projectRoot: TEMP_DIR },
'json'
);
// Call addTask with manual data to avoid AI
const { default: addTask } = await import(
'../../../scripts/modules/task-manager/add-task.js'
);
await addTask(
TASKS_PATH,
'Manual task',
[],
null,
{ projectRoot: TEMP_DIR, tag: 'feature-api' },
'json',
{
title: 'API work',
description: 'Implement endpoint',
details: 'Details',
testStrategy: 'Tests'
},
false
);
const data = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
expect(data['feature-api'].tasks.length).toBe(1); // the new task only
expect(data.master.tasks.length).toBe(1); // still only seed
});
it('reserved tag names are rejected', async () => {
await expect(
createTag(TASKS_PATH, 'master', {}, { projectRoot: TEMP_DIR }, 'json')
).rejects.toThrow(/reserved tag/i);
});
it('cannot delete the master tag', async () => {
await expect(
deleteTag(
TASKS_PATH,
'master',
{ yes: true },
{ projectRoot: TEMP_DIR },
'json'
)
).rejects.toThrow(/Cannot delete the "master" tag/);
});
it('copyTag deep copy – mutation does not affect source', async () => {
const { copyTag } = await import(
'../../../scripts/modules/task-manager/tag-management.js'
);
await createTag(
TASKS_PATH,
'source',
{ copyFromCurrent: true },
{ projectRoot: TEMP_DIR },
'json'
);
await copyTag(
TASKS_PATH,
'source',
'clone',
{},
{ projectRoot: TEMP_DIR },
'json'
);
// mutate clone task title
const data1 = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
data1.clone.tasks[0].title = 'Modified in clone';
fs.writeFileSync(TASKS_PATH, JSON.stringify(data1, null, 2));
const data2 = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
expect(data2.source.tasks[0].title).toBe('Seed task');
expect(data2.clone.tasks[0].title).toBe('Modified in clone');
});
it('adds task to tag derived from state.json when no explicit tag supplied', async () => {
// Create new tag and update state.json to make it current
await createTag(
TASKS_PATH,
'feature-auto',
{},
{ projectRoot: TEMP_DIR },
'json'
);
const state1 = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
state1.currentTag = 'feature-auto';
fs.writeFileSync(STATE_PATH, JSON.stringify(state1, null, 2));
const { default: addTask } = await import(
'../../../scripts/modules/task-manager/add-task.js'
);
const { resolveTag } = await import('../../../scripts/modules/utils.js');
const tag = resolveTag({ projectRoot: TEMP_DIR });
// Add task without passing tag -> should resolve to feature-auto
await addTask(
TASKS_PATH,
'Auto task',
[],
null,
{ projectRoot: TEMP_DIR, tag },
'json',
{
title: 'Auto task',
description: '-',
details: '-',
testStrategy: '-'
},
false
);
const data = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
expect(data['feature-auto'].tasks.length).toBe(1);
expect(data.master.tasks.length).toBe(1); // master unchanged
});
it('falls back to master when state.json lacks currentTag', async () => {
// wipe currentTag field
fs.writeFileSync(STATE_PATH, JSON.stringify({}, null, 2));
const { default: addTask } = await import(
'../../../scripts/modules/task-manager/add-task.js'
);
const { resolveTag } = await import('../../../scripts/modules/utils.js');
const tag = resolveTag({ projectRoot: TEMP_DIR }); // should return master
await addTask(
TASKS_PATH,
'Fallback task',
[],
null,
{ projectRoot: TEMP_DIR, tag },
'json',
{
title: 'Fallback',
description: '-',
details: '-',
testStrategy: '-'
},
false
);
const data = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
expect(data.master.tasks.length).toBe(2); // seed + new task
});
});
```
--------------------------------------------------------------------------------
/scripts/test-claude.js:
--------------------------------------------------------------------------------
```javascript
#!/usr/bin/env node
/**
* test-claude.js
*
* A simple test script to verify the improvements to the callClaude function.
* This script tests different scenarios:
* 1. Normal operation with a small PRD
* 2. Testing with a large number of tasks (to potentially trigger task reduction)
* 3. Simulating a failure to test retry logic
*/
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load environment variables from .env file
dotenv.config();
// Create a simple PRD for testing
const createTestPRD = (size = 'small', taskComplexity = 'simple') => {
let content = `# Test PRD - ${size.toUpperCase()} SIZE, ${taskComplexity.toUpperCase()} COMPLEXITY\n\n`;
// Add more content based on size
if (size === 'small') {
content += `
## Overview
This is a small test PRD to verify the callClaude function improvements.
## Requirements
1. Create a simple web application
2. Implement user authentication
3. Add a dashboard for users
4. Create an admin panel
5. Implement data visualization
## Technical Stack
- Frontend: React
- Backend: Node.js
- Database: MongoDB
`;
} else if (size === 'medium') {
// Medium-sized PRD with more requirements
content += `
## Overview
This is a medium-sized test PRD to verify the callClaude function improvements.
## Requirements
1. Create a web application with multiple pages
2. Implement user authentication with OAuth
3. Add a dashboard for users with customizable widgets
4. Create an admin panel with user management
5. Implement data visualization with charts and graphs
6. Add real-time notifications
7. Implement a search feature
8. Add user profile management
9. Implement role-based access control
10. Add a reporting system
11. Implement file uploads and management
12. Add a commenting system
13. Implement a rating system
14. Add a recommendation engine
15. Implement a payment system
## Technical Stack
- Frontend: React with TypeScript
- Backend: Node.js with Express
- Database: MongoDB with Mongoose
- Authentication: JWT and OAuth
- Deployment: Docker and Kubernetes
- CI/CD: GitHub Actions
- Monitoring: Prometheus and Grafana
`;
} else if (size === 'large') {
// Large PRD with many requirements
content += `
## Overview
This is a large test PRD to verify the callClaude function improvements.
## Requirements
`;
// Generate 30 requirements
for (let i = 1; i <= 30; i++) {
content += `${i}. Requirement ${i} - This is a detailed description of requirement ${i}.\n`;
}
content += `
## Technical Stack
- Frontend: React with TypeScript
- Backend: Node.js with Express
- Database: MongoDB with Mongoose
- Authentication: JWT and OAuth
- Deployment: Docker and Kubernetes
- CI/CD: GitHub Actions
- Monitoring: Prometheus and Grafana
## User Stories
`;
// Generate 20 user stories
for (let i = 1; i <= 20; i++) {
content += `- As a user, I want to be able to ${i} so that I can achieve benefit ${i}.\n`;
}
content += `
## Non-Functional Requirements
- Performance: The system should respond within 200ms
- Scalability: The system should handle 10,000 concurrent users
- Availability: The system should have 99.9% uptime
- Security: The system should comply with OWASP top 10
- Accessibility: The system should comply with WCAG 2.1 AA
`;
}
// Add complexity if needed
if (taskComplexity === 'complex') {
content += `
## Complex Requirements
- Implement a real-time collaboration system
- Add a machine learning-based recommendation engine
- Implement a distributed caching system
- Add a microservices architecture
- Implement a custom analytics engine
- Add support for multiple languages and locales
- Implement a custom search engine with advanced filtering
- Add a custom workflow engine
- Implement a custom reporting system
- Add a custom dashboard builder
`;
}
return content;
};
// Function to run the tests
async function runTests() {
console.log('Starting tests for callClaude function improvements...');
try {
// Instead of importing the callClaude function directly, we'll use the dev.js script
// with our test PRDs by running it as a child process
// Test 1: Small PRD, 5 tasks
console.log('\n=== Test 1: Small PRD, 5 tasks ===');
const smallPRD = createTestPRD('small', 'simple');
const smallPRDPath = path.join(__dirname, 'test-small-prd.txt');
fs.writeFileSync(smallPRDPath, smallPRD, 'utf8');
console.log(`Created test PRD at ${smallPRDPath}`);
console.log('Running dev.js with small PRD...');
// Use the child_process module to run the dev.js script
const { execSync } = await import('child_process');
try {
const smallResult = execSync(
`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${smallPRDPath} --num-tasks=5`,
{
stdio: 'inherit'
}
);
console.log('Small PRD test completed successfully');
} catch (error) {
console.error('Small PRD test failed:', error.message);
}
// Test 2: Medium PRD, 15 tasks
console.log('\n=== Test 2: Medium PRD, 15 tasks ===');
const mediumPRD = createTestPRD('medium', 'simple');
const mediumPRDPath = path.join(__dirname, 'test-medium-prd.txt');
fs.writeFileSync(mediumPRDPath, mediumPRD, 'utf8');
console.log(`Created test PRD at ${mediumPRDPath}`);
console.log('Running dev.js with medium PRD...');
try {
const mediumResult = execSync(
`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${mediumPRDPath} --num-tasks=15`,
{
stdio: 'inherit'
}
);
console.log('Medium PRD test completed successfully');
} catch (error) {
console.error('Medium PRD test failed:', error.message);
}
// Test 3: Large PRD, 25 tasks
console.log('\n=== Test 3: Large PRD, 25 tasks ===');
const largePRD = createTestPRD('large', 'complex');
const largePRDPath = path.join(__dirname, 'test-large-prd.txt');
fs.writeFileSync(largePRDPath, largePRD, 'utf8');
console.log(`Created test PRD at ${largePRDPath}`);
console.log('Running dev.js with large PRD...');
try {
const largeResult = execSync(
`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${largePRDPath} --num-tasks=25`,
{
stdio: 'inherit'
}
);
console.log('Large PRD test completed successfully');
} catch (error) {
console.error('Large PRD test failed:', error.message);
}
console.log('\nAll tests completed!');
} catch (error) {
console.error('Test failed:', error);
} finally {
// Clean up test files
console.log('\nCleaning up test files...');
const testFiles = [
path.join(__dirname, 'test-small-prd.txt'),
path.join(__dirname, 'test-medium-prd.txt'),
path.join(__dirname, 'test-large-prd.txt')
];
testFiles.forEach((file) => {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
console.log(`Deleted ${file}`);
}
});
console.log('Cleanup complete.');
}
}
// Run the tests
runTests().catch((error) => {
console.error('Error running tests:', error);
process.exit(1);
});
```
--------------------------------------------------------------------------------
/packages/claude-code-plugin/agents/task-orchestrator.md:
--------------------------------------------------------------------------------
```markdown
---
name: task-orchestrator
description: Use this agent when you need to coordinate and manage the execution of Task Master tasks, especially when dealing with complex task dependencies and parallel execution opportunities. This agent should be invoked at the beginning of a work session to analyze the task queue, identify parallelizable work, and orchestrate the deployment of task-executor agents. It should also be used when tasks complete to reassess the dependency graph and deploy new executors as needed.\n\n<example>\nContext: User wants to start working on their project tasks using Task Master\nuser: "Let's work on the next available tasks in the project"\nassistant: "I'll use the task-orchestrator agent to analyze the task queue and coordinate execution"\n<commentary>\nThe user wants to work on tasks, so the task-orchestrator should be deployed to analyze dependencies and coordinate execution.\n</commentary>\n</example>\n\n<example>\nContext: Multiple independent tasks are available in the queue\nuser: "Can we work on multiple tasks at once?"\nassistant: "Let me deploy the task-orchestrator to analyze task dependencies and parallelize the work"\n<commentary>\nWhen parallelization is mentioned or multiple tasks could be worked on, the orchestrator should coordinate the effort.\n</commentary>\n</example>\n\n<example>\nContext: A complex feature with many subtasks needs implementation\nuser: "Implement the authentication system tasks"\nassistant: "I'll use the task-orchestrator to break down the authentication tasks and coordinate their execution"\n<commentary>\nFor complex multi-task features, the orchestrator manages the overall execution strategy.\n</commentary>\n</example>
model: opus
color: green
---
You are the Task Orchestrator, an elite coordination agent specialized in managing Task Master workflows for maximum efficiency and parallelization. You excel at analyzing task dependency graphs, identifying opportunities for concurrent execution, and deploying specialized task-executor agents to complete work efficiently.
## Core Responsibilities
1. **Task Queue Analysis**: You continuously monitor and analyze the task queue using Task Master MCP tools to understand the current state of work, dependencies, and priorities.
2. **Dependency Graph Management**: You build and maintain a mental model of task dependencies, identifying which tasks can be executed in parallel and which must wait for prerequisites.
3. **Executor Deployment**: You strategically deploy task-executor agents for individual tasks or task groups, ensuring each executor has the necessary context and clear success criteria.
4. **Progress Coordination**: You track the progress of deployed executors, handle task completion notifications, and reassess the execution strategy as tasks complete.
## Operational Workflow
### Initial Assessment Phase
1. Use `get_tasks` or `task-master list` to retrieve all available tasks
2. Analyze task statuses, priorities, and dependencies
3. Identify tasks with status 'pending' that have no blocking dependencies
4. Group related tasks that could benefit from specialized executors
5. Create an execution plan that maximizes parallelization
### Executor Deployment Phase
1. For each independent task or task group:
- Deploy a task-executor agent with specific instructions
- Provide the executor with task ID, requirements, and context
- Set clear completion criteria and reporting expectations
2. Maintain a registry of active executors and their assigned tasks
3. Establish communication protocols for progress updates
### Coordination Phase
1. Monitor executor progress through task status updates
2. When a task completes:
- Verify completion with `get_task` or `task-master show <id>`
- Update task status if needed using `set_task_status`
- Reassess dependency graph for newly unblocked tasks
- Deploy new executors for available work
3. Handle executor failures or blocks:
- Reassign tasks to new executors if needed
- Escalate complex issues to the user
- Update task status to 'blocked' when appropriate
### Optimization Strategies
**Parallel Execution Rules**:
- Never assign dependent tasks to different executors simultaneously
- Prioritize high-priority tasks when resources are limited
- Group small, related subtasks for single executor efficiency
- Balance executor load to prevent bottlenecks
**Context Management**:
- Provide executors with minimal but sufficient context
- Share relevant completed task information when it aids execution
- Maintain a shared knowledge base of project-specific patterns
**Quality Assurance**:
- Verify task completion before marking as done
- Ensure test strategies are followed when specified
- Coordinate cross-task integration testing when needed
## Communication Protocols
When deploying executors, provide them with:
```
TASK ASSIGNMENT:
- Task ID: [specific ID]
- Objective: [clear goal]
- Dependencies: [list any completed prerequisites]
- Success Criteria: [specific completion requirements]
- Context: [relevant project information]
- Reporting: [when and how to report back]
```
When receiving executor updates:
1. Acknowledge completion or issues
2. Update task status in Task Master
3. Reassess execution strategy
4. Deploy new executors as appropriate
## Decision Framework
**When to parallelize**:
- Multiple pending tasks with no interdependencies
- Sufficient context available for independent execution
- Tasks are well-defined with clear success criteria
**When to serialize**:
- Strong dependencies between tasks
- Limited context or unclear requirements
- Integration points requiring careful coordination
**When to escalate**:
- Circular dependencies detected
- Critical blockers affecting multiple tasks
- Ambiguous requirements needing clarification
- Resource conflicts between executors
## Error Handling
1. **Executor Failure**: Reassign task to new executor with additional context about the failure
2. **Dependency Conflicts**: Halt affected executors, resolve conflict, then resume
3. **Task Ambiguity**: Request clarification from user before proceeding
4. **System Errors**: Implement graceful degradation, falling back to serial execution if needed
## Performance Metrics
Track and optimize for:
- Task completion rate
- Parallel execution efficiency
- Executor success rate
- Time to completion for task groups
- Dependency resolution speed
## Integration with Task Master
Leverage these Task Master MCP tools effectively:
- `get_tasks` - Continuous queue monitoring
- `get_task` - Detailed task analysis
- `set_task_status` - Progress tracking
- `next_task` - Fallback for serial execution
- `analyze_project_complexity` - Strategic planning
- `complexity_report` - Resource allocation
You are the strategic mind coordinating the entire task execution effort. Your success is measured by the efficient completion of all tasks while maintaining quality and respecting dependencies. Think systematically, act decisively, and continuously optimize the execution strategy based on real-time progress.
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/dependency-manager/circular-dependencies.test.js:
--------------------------------------------------------------------------------
```javascript
import { jest } from '@jest/globals';
import {
validateCrossTagMove,
findCrossTagDependencies,
getDependentTaskIds,
validateSubtaskMove,
canMoveWithDependencies
} from '../../../../../scripts/modules/dependency-manager.js';
describe('Circular Dependency Scenarios', () => {
describe('Circular Cross-Tag Dependencies', () => {
const allTasks = [
{
id: 1,
title: 'Task 1',
dependencies: [2],
status: 'pending',
tag: 'backlog'
},
{
id: 2,
title: 'Task 2',
dependencies: [3],
status: 'pending',
tag: 'backlog'
},
{
id: 3,
title: 'Task 3',
dependencies: [1],
status: 'pending',
tag: 'backlog'
}
];
it('should detect circular dependencies across tags', () => {
// Task 1 depends on 2, 2 depends on 3, 3 depends on 1 (circular)
// But since all tasks are in 'backlog' and target is 'in-progress',
// only direct dependencies that are in different tags will be found
const conflicts = findCrossTagDependencies(
[allTasks[0]],
'backlog',
'in-progress',
allTasks
);
// Only direct dependencies of task 1 that are not in target tag
expect(conflicts).toHaveLength(1);
expect(
conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
).toBe(true);
});
it('should block move with circular dependencies', () => {
// Since task 1 has dependencies in the same tag, validateCrossTagMove should not throw
// The function only checks direct dependencies, not circular chains
expect(() => {
validateCrossTagMove(allTasks[0], 'backlog', 'in-progress', allTasks);
}).not.toThrow();
});
it('should return canMove: false for circular dependencies', () => {
const result = canMoveWithDependencies(
'1',
'backlog',
'in-progress',
allTasks
);
expect(result.canMove).toBe(false);
expect(result.conflicts).toHaveLength(1);
});
});
describe('Complex Dependency Chains', () => {
const allTasks = [
{
id: 1,
title: 'Task 1',
dependencies: [2, 3],
status: 'pending',
tag: 'backlog'
},
{
id: 2,
title: 'Task 2',
dependencies: [4],
status: 'pending',
tag: 'backlog'
},
{
id: 3,
title: 'Task 3',
dependencies: [5],
status: 'pending',
tag: 'backlog'
},
{
id: 4,
title: 'Task 4',
dependencies: [],
status: 'pending',
tag: 'backlog'
},
{
id: 5,
title: 'Task 5',
dependencies: [6],
status: 'pending',
tag: 'backlog'
},
{
id: 6,
title: 'Task 6',
dependencies: [],
status: 'pending',
tag: 'backlog'
},
{
id: 7,
title: 'Task 7',
dependencies: [],
status: 'in-progress',
tag: 'in-progress'
}
];
it('should find all dependencies in complex chain', () => {
const conflicts = findCrossTagDependencies(
[allTasks[0]],
'backlog',
'in-progress',
allTasks
);
// Only direct dependencies of task 1 that are not in target tag
expect(conflicts).toHaveLength(2);
expect(
conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
).toBe(true);
expect(
conflicts.some((c) => c.taskId === 1 && c.dependencyId === 3)
).toBe(true);
});
it('should get all dependent task IDs in complex chain', () => {
const conflicts = findCrossTagDependencies(
[allTasks[0]],
'backlog',
'in-progress',
allTasks
);
const dependentIds = getDependentTaskIds(
[allTasks[0]],
conflicts,
allTasks
);
// Should include only the direct dependency IDs from conflicts
expect(dependentIds).toContain(2);
expect(dependentIds).toContain(3);
// Should not include the source task or tasks not in conflicts
expect(dependentIds).not.toContain(1);
});
});
describe('Mixed Dependency Types', () => {
const allTasks = [
{
id: 1,
title: 'Task 1',
dependencies: [2, '3.1'],
status: 'pending',
tag: 'backlog'
},
{
id: 2,
title: 'Task 2',
dependencies: [4],
status: 'pending',
tag: 'backlog'
},
{
id: 3,
title: 'Task 3',
dependencies: [5],
status: 'pending',
tag: 'backlog',
subtasks: [
{
id: 1,
title: 'Subtask 3.1',
dependencies: [],
status: 'pending',
tag: 'backlog'
}
]
},
{
id: 4,
title: 'Task 4',
dependencies: [],
status: 'pending',
tag: 'backlog'
},
{
id: 5,
title: 'Task 5',
dependencies: [],
status: 'pending',
tag: 'backlog'
}
];
it('should handle mixed task and subtask dependencies', () => {
const conflicts = findCrossTagDependencies(
[allTasks[0]],
'backlog',
'in-progress',
allTasks
);
expect(conflicts).toHaveLength(2);
expect(
conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
).toBe(true);
expect(
conflicts.some((c) => c.taskId === 1 && c.dependencyId === '3.1')
).toBe(true);
});
});
describe('Large Task Set Performance', () => {
const allTasks = [];
for (let i = 1; i <= 100; i++) {
allTasks.push({
id: i,
title: `Task ${i}`,
dependencies: i < 100 ? [i + 1] : [],
status: 'pending',
tag: 'backlog'
});
}
it('should handle large task sets efficiently', () => {
const conflicts = findCrossTagDependencies(
[allTasks[0]],
'backlog',
'in-progress',
allTasks
);
expect(conflicts.length).toBeGreaterThan(0);
expect(conflicts[0]).toHaveProperty('taskId');
expect(conflicts[0]).toHaveProperty('dependencyId');
});
});
describe('Edge Cases and Error Conditions', () => {
const allTasks = [
{
id: 1,
title: 'Task 1',
dependencies: [2],
status: 'pending',
tag: 'backlog'
},
{
id: 2,
title: 'Task 2',
dependencies: [],
status: 'pending',
tag: 'backlog'
}
];
it('should handle empty task arrays', () => {
expect(() => {
findCrossTagDependencies([], 'backlog', 'in-progress', allTasks);
}).not.toThrow();
});
it('should handle non-existent tasks gracefully', () => {
expect(() => {
findCrossTagDependencies(
[{ id: 999, dependencies: [] }],
'backlog',
'in-progress',
allTasks
);
}).not.toThrow();
});
it('should handle invalid tag names', () => {
expect(() => {
findCrossTagDependencies(
[allTasks[0]],
'invalid-tag',
'in-progress',
allTasks
);
}).not.toThrow();
});
it('should handle null/undefined dependencies', () => {
const taskWithNullDeps = {
...allTasks[0],
dependencies: [null, undefined, 2]
};
expect(() => {
findCrossTagDependencies(
[taskWithNullDeps],
'backlog',
'in-progress',
allTasks
);
}).not.toThrow();
});
it('should handle string dependencies correctly', () => {
const taskWithStringDeps = { ...allTasks[0], dependencies: ['2', '3'] };
const conflicts = findCrossTagDependencies(
[taskWithStringDeps],
'backlog',
'in-progress',
allTasks
);
expect(conflicts.length).toBeGreaterThanOrEqual(0);
});
});
});
```
--------------------------------------------------------------------------------
/apps/cli/tests/unit/ui/dashboard.component.spec.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Tests for dashboard component calculations
* Bug fix: Cancelled tasks should be treated as complete
*/
import type { Task } from '@tm/core';
import { describe, expect, it } from 'vitest';
import {
type TaskStatistics,
calculateDependencyStatistics,
calculateSubtaskStatistics,
calculateTaskStatistics
} from '../../../src/ui/components/dashboard.component.js';
describe('dashboard.component - Bug Fix: Cancelled Tasks as Complete', () => {
describe('calculateTaskStatistics', () => {
it('should treat cancelled tasks as complete in percentage calculation', () => {
// Arrange: 14 done, 1 cancelled = 100% complete
const tasks: Task[] = [
...Array.from({ length: 14 }, (_, i) => ({
id: i + 1,
title: `Task ${i + 1}`,
status: 'done' as const,
dependencies: []
})),
{
id: 15,
title: 'Cancelled Task',
status: 'cancelled' as const,
dependencies: []
}
];
// Act
const stats = calculateTaskStatistics(tasks);
// Assert
expect(stats.total).toBe(15);
expect(stats.done).toBe(14);
expect(stats.cancelled).toBe(1);
expect(stats.completedCount).toBe(15); // done + cancelled
// BUG: Current code shows 93% (14/15), should be 100% (15/15)
expect(stats.completionPercentage).toBe(100);
});
it('should treat completed status as complete in percentage calculation', () => {
// Arrange: Mix of done, completed, cancelled
const tasks: Task[] = [
{
id: 1,
title: 'Done Task',
status: 'done' as const,
dependencies: []
},
{
id: 2,
title: 'Completed Task',
status: 'completed' as const,
dependencies: []
},
{
id: 3,
title: 'Cancelled Task',
status: 'cancelled' as const,
dependencies: []
},
{
id: 4,
title: 'Pending Task',
status: 'pending' as const,
dependencies: []
}
];
// Act
const stats = calculateTaskStatistics(tasks);
// Assert
expect(stats.total).toBe(4);
expect(stats.done).toBe(1);
expect(stats.cancelled).toBe(1);
expect(stats.completedCount).toBe(3); // done + completed + cancelled
// 3 complete out of 4 total = 75%
expect(stats.completionPercentage).toBe(75);
});
it('should show 100% completion when all tasks are cancelled', () => {
// Arrange
const tasks: Task[] = [
{
id: 1,
title: 'Cancelled 1',
status: 'cancelled' as const,
dependencies: []
},
{
id: 2,
title: 'Cancelled 2',
status: 'cancelled' as const,
dependencies: []
}
];
// Act
const stats = calculateTaskStatistics(tasks);
// Assert
expect(stats.total).toBe(2);
expect(stats.cancelled).toBe(2);
expect(stats.completedCount).toBe(2); // All cancelled = all complete
// BUG: Current code shows 0%, should be 100%
expect(stats.completionPercentage).toBe(100);
});
it('should show 0% completion when no tasks are complete', () => {
// Arrange
const tasks: Task[] = [
{
id: 1,
title: 'Pending Task',
status: 'pending' as const,
dependencies: []
},
{
id: 2,
title: 'In Progress Task',
status: 'in-progress' as const,
dependencies: []
}
];
// Act
const stats = calculateTaskStatistics(tasks);
// Assert
expect(stats.completionPercentage).toBe(0);
});
});
describe('calculateSubtaskStatistics', () => {
it('should treat cancelled subtasks as complete in percentage calculation', () => {
// Arrange: Task with 3 done subtasks and 1 cancelled = 100%
const tasks: Task[] = [
{
id: 1,
title: 'Parent Task',
status: 'in-progress' as const,
dependencies: [],
subtasks: [
{ id: '1', title: 'Sub 1', status: 'done' },
{ id: '2', title: 'Sub 2', status: 'done' },
{ id: '3', title: 'Sub 3', status: 'done' },
{ id: '4', title: 'Sub 4', status: 'cancelled' }
]
}
];
// Act
const stats = calculateSubtaskStatistics(tasks);
// Assert
expect(stats.total).toBe(4);
expect(stats.done).toBe(3);
expect(stats.cancelled).toBe(1);
expect(stats.completedCount).toBe(4); // done + cancelled
// BUG: Current code shows 75% (3/4), should be 100% (4/4)
expect(stats.completionPercentage).toBe(100);
});
it('should handle completed status in subtasks', () => {
// Arrange
const tasks: Task[] = [
{
id: 1,
title: 'Parent Task',
status: 'in-progress' as const,
dependencies: [],
subtasks: [
{ id: '1', title: 'Sub 1', status: 'done' },
{ id: '2', title: 'Sub 2', status: 'completed' },
{ id: '3', title: 'Sub 3', status: 'pending' }
]
}
];
// Act
const stats = calculateSubtaskStatistics(tasks);
// Assert
expect(stats.total).toBe(3);
expect(stats.completedCount).toBe(2); // done + completed
// 2 complete (done + completed) out of 3 = 67%
expect(stats.completionPercentage).toBe(67);
});
});
describe('calculateDependencyStatistics', () => {
it('should treat cancelled tasks as satisfied dependencies', () => {
// Arrange: Task 15 depends on cancelled task 14
const tasks: Task[] = [
...Array.from({ length: 13 }, (_, i) => ({
id: i + 1,
title: `Task ${i + 1}`,
status: 'done' as const,
dependencies: []
})),
{
id: 14,
title: 'Cancelled Dependency',
status: 'cancelled' as const,
dependencies: []
},
{
id: 15,
title: 'Dependent Task',
status: 'pending' as const,
dependencies: [14]
}
];
// Act
const stats = calculateDependencyStatistics(tasks);
// Assert
// Task 15 should be ready to work on since its dependency (14) is cancelled
// BUG: Current code shows task 15 as blocked, should show as ready
expect(stats.tasksBlockedByDeps).toBe(0);
expect(stats.tasksReadyToWork).toBeGreaterThan(0);
});
it('should treat completed status as satisfied dependencies', () => {
// Arrange
const tasks: Task[] = [
{
id: 1,
title: 'Completed Dependency',
status: 'completed' as const,
dependencies: []
},
{
id: 2,
title: 'Dependent Task',
status: 'pending' as const,
dependencies: [1]
}
];
// Act
const stats = calculateDependencyStatistics(tasks);
// Assert
expect(stats.tasksBlockedByDeps).toBe(0);
expect(stats.tasksReadyToWork).toBe(1);
});
it('should count tasks with cancelled dependencies as ready', () => {
// Arrange: Multiple tasks depending on cancelled tasks
const tasks: Task[] = [
{
id: 1,
title: 'Cancelled Task',
status: 'cancelled' as const,
dependencies: []
},
{
id: 2,
title: 'Dependent 1',
status: 'pending' as const,
dependencies: [1]
},
{
id: 3,
title: 'Dependent 2',
status: 'pending' as const,
dependencies: [1]
}
];
// Act
const stats = calculateDependencyStatistics(tasks);
// Assert
expect(stats.tasksBlockedByDeps).toBe(0);
expect(stats.tasksReadyToWork).toBe(2); // Both dependents should be ready
});
});
});
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/managers/config-manager.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Configuration Manager
* Orchestrates configuration services following clean architecture principles
*
* This ConfigManager delegates responsibilities to specialized services for better
* maintainability, testability, and separation of concerns.
*/
import type {
PartialConfiguration,
RuntimeStorageConfig
} from '../../../common/interfaces/configuration.interface.js';
import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../../../common/interfaces/configuration.interface.js';
import { ConfigLoader } from '../services/config-loader.service.js';
import {
CONFIG_PRECEDENCE,
ConfigMerger
} from '../services/config-merger.service.js';
import { ConfigPersistence } from '../services/config-persistence.service.js';
import { EnvironmentConfigProvider } from '../services/environment-config-provider.service.js';
import { RuntimeStateManager } from '../services/runtime-state-manager.service.js';
/**
* ConfigManager orchestrates all configuration services
*
* This class delegates responsibilities to specialized services:
* - ConfigLoader: Loads configuration from files
* - ConfigMerger: Merges configurations with precedence
* - RuntimeStateManager: Manages runtime state
* - ConfigPersistence: Handles file persistence
* - EnvironmentConfigProvider: Extracts env var configuration
*/
export class ConfigManager {
private projectRoot: string;
private config: PartialConfiguration = {};
private initialized = false;
// Services
private loader: ConfigLoader;
private merger: ConfigMerger;
private stateManager: RuntimeStateManager;
private persistence: ConfigPersistence;
private envProvider: EnvironmentConfigProvider;
/**
* Create and initialize a new ConfigManager instance
* This is the ONLY way to create a ConfigManager
*
* @param projectRoot - The root directory of the project
* @returns Fully initialized ConfigManager instance
*/
static async create(projectRoot: string): Promise<ConfigManager> {
const manager = new ConfigManager(projectRoot);
await manager.initialize();
return manager;
}
/**
* Private constructor - use ConfigManager.create() instead
* This ensures the ConfigManager is always properly initialized
*/
private constructor(projectRoot: string) {
this.projectRoot = projectRoot;
// Initialize services
this.loader = new ConfigLoader(projectRoot);
this.merger = new ConfigMerger();
this.stateManager = new RuntimeStateManager(projectRoot);
this.persistence = new ConfigPersistence(projectRoot);
this.envProvider = new EnvironmentConfigProvider();
}
/**
* Initialize by loading configuration from all sources
* Private - only called by the factory method
*/
private async initialize(): Promise<void> {
if (this.initialized) return;
// Clear any existing configuration sources
this.merger.clearSources();
// 1. Load default configuration (lowest precedence)
this.merger.addSource({
name: 'defaults',
config: this.loader.getDefaultConfig(),
precedence: CONFIG_PRECEDENCE.DEFAULTS
});
// 2. Load global configuration (if exists)
const globalConfig = await this.loader.loadGlobalConfig();
if (globalConfig) {
this.merger.addSource({
name: 'global',
config: globalConfig,
precedence: CONFIG_PRECEDENCE.GLOBAL
});
}
// 3. Load local project configuration
const localConfig = await this.loader.loadLocalConfig();
if (localConfig) {
this.merger.addSource({
name: 'local',
config: localConfig,
precedence: CONFIG_PRECEDENCE.LOCAL
});
}
// 4. Load environment variables (highest precedence)
const envConfig = this.envProvider.loadConfig();
if (Object.keys(envConfig).length > 0) {
this.merger.addSource({
name: 'environment',
config: envConfig,
precedence: CONFIG_PRECEDENCE.ENVIRONMENT
});
}
// 5. Merge all configurations
this.config = this.merger.merge();
// 6. Load runtime state
await this.stateManager.loadState();
this.initialized = true;
}
// ==================== Configuration Access ====================
/**
* Get full configuration
*/
getConfig(): PartialConfiguration {
return this.config;
}
/**
* Get storage configuration
*/
getStorageConfig(): RuntimeStorageConfig {
const storage = this.config.storage;
// Return the configured type (including 'auto')
const storageType = storage?.type || 'auto';
const basePath = storage?.basePath ?? this.projectRoot;
if (storageType === 'api' || storageType === 'auto') {
return {
type: storageType,
basePath,
apiEndpoint: storage?.apiEndpoint,
apiAccessToken: storage?.apiAccessToken,
apiConfigured: Boolean(storage?.apiEndpoint || storage?.apiAccessToken)
};
}
return {
type: storageType,
basePath,
apiConfigured: false
};
}
/**
* Get model configuration
*/
getModelConfig() {
return (
this.config.models || {
main: DEFAULTS.MODELS.MAIN,
fallback: DEFAULTS.MODELS.FALLBACK
}
);
}
/**
* Get response language setting
*/
getResponseLanguage(): string {
const customConfig = this.config.custom as any;
return customConfig?.responseLanguage || 'English';
}
/**
* Get project root path
*/
getProjectRoot(): string {
return this.projectRoot;
}
/**
* Check if explicitly configured to use API storage
* Excludes 'auto' type
*/
isApiExplicitlyConfigured(): boolean {
return this.getStorageConfig().type === 'api';
}
// ==================== Runtime State ====================
/**
* Get the currently active tag
*/
getActiveTag(): string {
return this.stateManager.getCurrentTag();
}
/**
* Set the active tag
*/
async setActiveTag(tag: string): Promise<void> {
await this.stateManager.setCurrentTag(tag);
}
// ==================== Configuration Updates ====================
/**
* Update configuration
*/
async updateConfig(updates: PartialConfiguration): Promise<void> {
// Merge updates into current config
Object.assign(this.config, updates);
// Save to persistence
await this.persistence.saveConfig(this.config);
// Re-initialize to respect precedence
this.initialized = false;
await this.initialize();
}
/**
* Set response language
*/
async setResponseLanguage(language: string): Promise<void> {
if (!this.config.custom) {
this.config.custom = {};
}
(this.config.custom as any).responseLanguage = language;
await this.persistence.saveConfig(this.config);
}
/**
* Save current configuration
*/
async saveConfig(): Promise<void> {
await this.persistence.saveConfig(this.config, {
createBackup: true,
atomic: true
});
}
// ==================== Utilities ====================
/**
* Reset configuration to defaults
*/
async reset(): Promise<void> {
// Clear configuration file
await this.persistence.deleteConfig();
// Clear runtime state
await this.stateManager.clearState();
// Reset internal state
this.initialized = false;
this.config = {};
// Re-initialize with defaults
await this.initialize();
}
/**
* Get configuration sources for debugging
*/
getConfigSources() {
return this.merger.getSources();
}
}
```
--------------------------------------------------------------------------------
/packages/tm-core/src/modules/auth/services/supabase-session-storage.spec.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Tests for SupabaseSessionStorage
* Verifies session persistence with steno atomic writes
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import fs from 'fs/promises';
import fsSync from 'fs';
import os from 'os';
import path from 'path';
import { SupabaseSessionStorage } from './supabase-session-storage.js';
describe('SupabaseSessionStorage', () => {
let tempDir: string;
let sessionPath: string;
beforeEach(() => {
// Create unique temp directory for each test
tempDir = fsSync.mkdtempSync(path.join(os.tmpdir(), 'tm-session-test-'));
sessionPath = path.join(tempDir, 'session.json');
});
afterEach(() => {
// Clean up temp directory
if (fsSync.existsSync(tempDir)) {
fsSync.rmSync(tempDir, { recursive: true, force: true });
}
});
describe('persistence with steno', () => {
it('should immediately persist data to disk with setItem', async () => {
const storage = new SupabaseSessionStorage(sessionPath);
// Set a session token
const testSession = JSON.stringify({
access_token: 'test-token',
refresh_token: 'test-refresh'
});
await storage.setItem('sb-localhost-auth-token', testSession);
// Immediately verify file exists and is readable (without any delay)
expect(fsSync.existsSync(sessionPath)).toBe(true);
// Read directly from disk (simulating a new process)
const diskData = JSON.parse(fsSync.readFileSync(sessionPath, 'utf8'));
expect(diskData['sb-localhost-auth-token']).toBe(testSession);
});
it('should guarantee data is on disk before returning', async () => {
const storage = new SupabaseSessionStorage(sessionPath);
// Write large session data
const largeSession = JSON.stringify({
access_token: 'x'.repeat(10000),
refresh_token: 'y'.repeat(10000),
user: { id: 'test', email: '[email protected]' }
});
await storage.setItem('sb-localhost-auth-token', largeSession);
// Create a NEW storage instance (simulates separate CLI command)
const newStorage = new SupabaseSessionStorage(sessionPath);
// Should immediately read the persisted data
const retrieved = await newStorage.getItem('sb-localhost-auth-token');
expect(retrieved).toBe(largeSession);
});
it('should handle rapid sequential writes without data loss', async () => {
const storage = new SupabaseSessionStorage(sessionPath);
// Simulate rapid token updates (like during refresh)
for (let i = 0; i < 5; i++) {
const session = JSON.stringify({
access_token: `token-${i}`,
expires_at: Date.now() + i * 1000
});
await storage.setItem('sb-localhost-auth-token', session);
// Each write should be immediately readable
const newStorage = new SupabaseSessionStorage(sessionPath);
const retrieved = await newStorage.getItem('sb-localhost-auth-token');
expect(JSON.parse(retrieved!).access_token).toBe(`token-${i}`);
}
});
});
describe('atomic writes', () => {
it('should complete writes atomically without leaving temp files', async () => {
const storage = new SupabaseSessionStorage(sessionPath);
await storage.setItem('test-key', 'test-value');
// Final file should exist with correct content
expect(fsSync.existsSync(sessionPath)).toBe(true);
const diskData = JSON.parse(fsSync.readFileSync(sessionPath, 'utf8'));
expect(diskData['test-key']).toBe('test-value');
// No unexpected extra files should remain in directory
const files = fsSync.readdirSync(path.dirname(sessionPath));
expect(files).toEqual([path.basename(sessionPath)]);
});
it('should maintain correct file permissions (0700 for directory)', async () => {
const storage = new SupabaseSessionStorage(sessionPath);
await storage.setItem('test-key', 'test-value');
// Check that file exists
expect(fsSync.existsSync(sessionPath)).toBe(true);
// Check directory has correct permissions
const dir = path.dirname(sessionPath);
const stats = fsSync.statSync(dir);
const mode = stats.mode & 0o777;
// Directory should be readable/writable/executable by owner only (0700)
expect(mode).toBe(0o700);
});
});
describe('basic operations', () => {
it('should get and set items', async () => {
const storage = new SupabaseSessionStorage(sessionPath);
await storage.setItem('key1', 'value1');
expect(await storage.getItem('key1')).toBe('value1');
});
it('should return null for non-existent items', async () => {
const storage = new SupabaseSessionStorage(sessionPath);
expect(await storage.getItem('non-existent')).toBe(null);
});
it('should remove items', async () => {
const storage = new SupabaseSessionStorage(sessionPath);
await storage.setItem('key1', 'value1');
expect(await storage.getItem('key1')).toBe('value1');
await storage.removeItem('key1');
expect(await storage.getItem('key1')).toBe(null);
// Verify removed from disk
const newStorage = new SupabaseSessionStorage(sessionPath);
expect(await newStorage.getItem('key1')).toBe(null);
});
});
describe('initialization', () => {
it('should load existing session on initialization', async () => {
// Create initial storage and save data
const storage1 = new SupabaseSessionStorage(sessionPath);
await storage1.setItem('key1', 'value1');
// Create new instance (simulates new process)
const storage2 = new SupabaseSessionStorage(sessionPath);
expect(await storage2.getItem('key1')).toBe('value1');
});
it('should handle non-existent session file gracefully', async () => {
// Don't create any file, just initialize
const storage = new SupabaseSessionStorage(sessionPath);
// Should not throw and should work normally
expect(await storage.getItem('any-key')).toBe(null);
await storage.setItem('key1', 'value1');
expect(await storage.getItem('key1')).toBe('value1');
});
it('should create directory if it does not exist', async () => {
const deepPath = path.join(
tempDir,
'deep',
'nested',
'path',
'session.json'
);
const storage = new SupabaseSessionStorage(deepPath);
await storage.setItem('key1', 'value1');
expect(fsSync.existsSync(deepPath)).toBe(true);
expect(fsSync.existsSync(path.dirname(deepPath))).toBe(true);
});
});
describe('error handling', () => {
it('should not throw on persist errors', async () => {
const invalidPath = '/invalid/path/that/cannot/be/written/session.json';
const storage = new SupabaseSessionStorage(invalidPath);
// Should not throw, session should remain in memory
await storage.setItem('key1', 'value1');
// Should still work in memory
expect(await storage.getItem('key1')).toBe('value1');
});
it('should handle corrupted session file gracefully', async () => {
// Write corrupted JSON
fsSync.writeFileSync(sessionPath, 'invalid json {{{');
// Should not throw on initialization
expect(() => new SupabaseSessionStorage(sessionPath)).not.toThrow();
// Should work normally after initialization
const storage = new SupabaseSessionStorage(sessionPath);
await storage.setItem('key1', 'value1');
expect(await storage.getItem('key1')).toBe('value1');
});
});
});
```
--------------------------------------------------------------------------------
/apps/mcp/src/shared/utils.ts:
--------------------------------------------------------------------------------
```typescript
/**
* Shared utilities for MCP tools
*/
import type { ContentResult } from 'fastmcp';
import path from 'node:path';
import fs from 'node:fs';
import packageJson from '../../../../package.json' with { type: 'json' };
/**
* Get version information
*/
export function getVersionInfo() {
return {
version: packageJson.version || 'unknown',
name: packageJson.name || 'task-master-ai'
};
}
/**
* Get current tag for a project root
*/
export function getCurrentTag(projectRoot: string): string | null {
try {
// Try to read current tag from state.json
const stateJsonPath = path.join(projectRoot, '.taskmaster', 'state.json');
if (fs.existsSync(stateJsonPath)) {
const stateData = JSON.parse(fs.readFileSync(stateJsonPath, 'utf-8'));
return stateData.currentTag || 'master';
}
return 'master';
} catch {
return null;
}
}
/**
* Handle API result with standardized error handling and response formatting
* This provides a consistent response structure for all MCP tools
*/
export async function handleApiResult<T>(options: {
result: { success: boolean; data?: T; error?: { message: string } };
log?: any;
errorPrefix?: string;
projectRoot?: string;
tag?: string; // Optional tag/brief to use instead of reading from state.json
}): Promise<ContentResult> {
const {
result,
log,
errorPrefix = 'API error',
projectRoot,
tag: providedTag
} = options;
// Get version info for every response
const versionInfo = getVersionInfo();
// Use provided tag if available, otherwise get from state.json
// Note: For API storage, tm-core returns the brief name as the tag
const currentTag =
providedTag !== undefined
? providedTag
: projectRoot
? getCurrentTag(projectRoot)
: null;
if (!result.success) {
const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
log?.error?.(`${errorPrefix}: ${errorMsg}`);
let errorText = `Error: ${errorMsg}\nVersion: ${versionInfo.version}\nName: ${versionInfo.name}`;
if (currentTag) {
errorText += `\nCurrent Tag: ${currentTag}`;
}
return {
content: [
{
type: 'text',
text: errorText
}
],
isError: true
};
}
log?.info?.('Successfully completed operation');
// Create the response payload including version info and tag
const responsePayload: any = {
data: result.data,
version: versionInfo
};
// Add current tag if available
if (currentTag) {
responsePayload.tag = currentTag;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(responsePayload, null, 2)
}
]
};
}
/**
* Normalize project root path (handles URI encoding, file:// protocol, Windows paths)
*/
export function normalizeProjectRoot(rawPath: string): string {
if (!rawPath) return process.cwd();
try {
let pathString = rawPath;
// Decode URI encoding
try {
pathString = decodeURIComponent(pathString);
} catch {
// If decoding fails, use as-is
}
// Strip file:// prefix
if (pathString.startsWith('file:///')) {
pathString = pathString.slice(7);
} else if (pathString.startsWith('file://')) {
pathString = pathString.slice(7);
}
// Handle Windows drive letter after stripping prefix (e.g., /C:/...)
if (
pathString.startsWith('/') &&
/[A-Za-z]:/.test(pathString.substring(1, 3))
) {
pathString = pathString.substring(1);
}
// Normalize backslashes to forward slashes
pathString = pathString.replace(/\\/g, '/');
// Resolve to absolute path
return path.resolve(pathString);
} catch {
return path.resolve(rawPath);
}
}
/**
* Get project root from session object
*/
function getProjectRootFromSession(session: any): string | null {
try {
// Check primary location
if (session?.roots?.[0]?.uri) {
return normalizeProjectRoot(session.roots[0].uri);
}
// Check alternate location
else if (session?.roots?.roots?.[0]?.uri) {
return normalizeProjectRoot(session.roots.roots[0].uri);
}
return null;
} catch {
return null;
}
}
/**
* Wrapper to normalize project root in args with proper precedence order
*
* PRECEDENCE ORDER:
* 1. TASK_MASTER_PROJECT_ROOT environment variable (from process.env or session)
* 2. args.projectRoot (explicitly provided)
* 3. Session-based project root resolution
* 4. Current directory fallback
*/
export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
fn: (
args: T & { projectRoot: string },
context: any
) => Promise<ContentResult>
): (args: T, context: any) => Promise<ContentResult> {
return async (args: T, context: any): Promise<ContentResult> => {
const { log, session } = context;
let normalizedRoot: string | null = null;
let rootSource = 'unknown';
try {
// 1. Check for TASK_MASTER_PROJECT_ROOT environment variable first
if (process.env.TASK_MASTER_PROJECT_ROOT) {
const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
normalizedRoot = path.isAbsolute(envRoot)
? envRoot
: path.resolve(process.cwd(), envRoot);
rootSource = 'TASK_MASTER_PROJECT_ROOT environment variable';
log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
}
// Also check session environment variables for TASK_MASTER_PROJECT_ROOT
else if (session?.env?.TASK_MASTER_PROJECT_ROOT) {
const envRoot = session.env.TASK_MASTER_PROJECT_ROOT;
normalizedRoot = path.isAbsolute(envRoot)
? envRoot
: path.resolve(process.cwd(), envRoot);
rootSource = 'TASK_MASTER_PROJECT_ROOT session environment variable';
log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
}
// 2. If no environment variable, try args.projectRoot
else if (args.projectRoot) {
normalizedRoot = normalizeProjectRoot(args.projectRoot);
rootSource = 'args.projectRoot';
log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
}
// 3. If no args.projectRoot, try session-based resolution
else {
const sessionRoot = getProjectRootFromSession(session);
if (sessionRoot) {
normalizedRoot = sessionRoot;
rootSource = 'session';
log?.info?.(
`Using project root from ${rootSource}: ${normalizedRoot}`
);
}
}
if (!normalizedRoot) {
log?.error?.(
'Could not determine project root from environment, args, or session.'
);
return handleApiResult({
result: {
success: false,
error: {
message:
'Could not determine project root. Please provide projectRoot argument or ensure TASK_MASTER_PROJECT_ROOT environment variable is set.'
}
}
});
}
// Inject the normalized root back into args
const updatedArgs = { ...args, projectRoot: normalizedRoot } as T & {
projectRoot: string;
};
// Execute the original function with normalized root in args
return await fn(updatedArgs, context);
} catch (error: any) {
log?.error?.(
`Error within withNormalizedProjectRoot HOF (Normalized Root: ${normalizedRoot}): ${error.message}`
);
if (error.stack && log?.debug) {
log.debug(error.stack);
}
return handleApiResult({
result: {
success: false,
error: {
message: `Operation failed: ${error.message}`
}
}
});
}
};
}
```
--------------------------------------------------------------------------------
/.taskmaster/reports/task-complexity-report_tdd-workflow-phase-0.json:
--------------------------------------------------------------------------------
```json
{
"meta": {
"generatedAt": "2025-10-07T14:16:40.283Z",
"tasksAnalyzed": 10,
"totalTasks": 10,
"analysisCount": 10,
"thresholdScore": 5,
"projectName": "Taskmaster",
"usedResearch": false
},
"complexityAnalysis": [
{
"taskId": 1,
"taskTitle": "Create autopilot command CLI skeleton",
"complexityScore": 4,
"recommendedSubtasks": 3,
"expansionPrompt": "Break down the autopilot command creation into: 1) Create AutopilotCommand class extending Commander.Command with proper argument parsing and options, 2) Implement command structure with help text and validation following existing patterns, 3) Add basic registration method and placeholder action handler",
"reasoning": "Medium complexity due to following established patterns in the codebase. The command-registry.ts and start.command.ts provide clear templates for implementation. Main complexity is argument parsing and option validation."
},
{
"taskId": 2,
"taskTitle": "Implement preflight detection system",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Create PreflightChecker with these subtasks: 1) Package.json test script detection and validation, 2) Git working tree status checking using system commands, 3) Tool availability validation (git, gh, node/npm), 4) Default branch detection via git commands, 5) Structured result reporting with success/failure indicators and error messages",
"reasoning": "High complexity due to system integration requirements. Needs to interact with multiple external tools (git, npm, gh), parse various file formats, and handle different system configurations. Error handling for missing tools adds complexity."
},
{
"taskId": 3,
"taskTitle": "Implement task loading and validation",
"complexityScore": 5,
"recommendedSubtasks": 3,
"expansionPrompt": "Implement task loading: 1) Use existing TaskService from @tm/core to load tasks by ID with proper error handling, 2) Validate task structure including subtask existence and dependency validation, 3) Provide user-friendly error messages for missing tasks or need to expand subtasks first",
"reasoning": "Medium-high complexity. While leveraging existing TaskService reduces implementation effort, the validation logic for subtasks and dependencies requires careful handling of edge cases. Task structure validation adds complexity."
},
{
"taskId": 4,
"taskTitle": "Create execution plan display logic",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Build ExecutionPlanDisplay: 1) Create display formatter using boxen and chalk for consistent CLI styling, 2) Format preflight check results with color-coded status indicators, 3) Display subtask execution order with RED/GREEN/COMMIT phase visualization, 4) Show branch/tag info and finalization steps with duration estimates",
"reasoning": "Moderate-high complexity due to complex formatting requirements and dependency on multiple other components. The display needs to coordinate information from preflight, task validation, and execution planning. CLI styling consistency adds complexity."
},
{
"taskId": 5,
"taskTitle": "Implement branch and tag planning",
"complexityScore": 3,
"recommendedSubtasks": 2,
"expansionPrompt": "Create BranchPlanner: 1) Implement branch name generation using pattern <tag>/task-<id>-<slug> with kebab-case conversion and special character handling, 2) Add TaskMaster config integration to determine active tag and handle existing branch conflicts",
"reasoning": "Low-medium complexity. String manipulation and naming convention implementation is straightforward. The main complexity is handling edge cases with special characters and existing branch conflicts."
},
{
"taskId": 6,
"taskTitle": "Create subtask execution order calculation",
"complexityScore": 8,
"recommendedSubtasks": 4,
"expansionPrompt": "Implement dependency resolution: 1) Build dependency graph from subtask data with proper parsing, 2) Implement topological sort algorithm for execution order, 3) Add circular dependency detection with clear error reporting, 4) Create parallel execution grouping for independent subtasks",
"reasoning": "High complexity due to graph algorithms and dependency resolution. Topological sorting, circular dependency detection, and parallel grouping require algorithmic sophistication. Edge cases in dependency chains add significant complexity."
},
{
"taskId": 7,
"taskTitle": "Implement TDD phase planning for subtasks",
"complexityScore": 6,
"recommendedSubtasks": 4,
"expansionPrompt": "Create TDDPhasePlanner: 1) Implement test file path detection for common project structures (src/, tests/, __tests__), 2) Parse implementation files from subtask details and descriptions, 3) Generate conventional commit messages for RED/GREEN/COMMIT phases, 4) Add implementation complexity estimation based on subtask content",
"reasoning": "Moderate-high complexity due to project structure detection and file path inference. Conventional commit message generation and complexity estimation require understanding of different project layouts and parsing subtask content effectively."
},
{
"taskId": 8,
"taskTitle": "Add finalization steps planning",
"complexityScore": 4,
"recommendedSubtasks": 3,
"expansionPrompt": "Create FinalizationPlanner: 1) Implement test suite execution planning with coverage threshold detection from package.json, 2) Add git operations planning (branch push, PR creation) using existing git patterns, 3) Create duration estimation algorithm based on subtask count and complexity metrics",
"reasoning": "Medium complexity. Building on existing git utilities and test command detection reduces complexity. Main challenges are coverage threshold parsing and duration estimation algorithms."
},
{
"taskId": 9,
"taskTitle": "Integrate command with existing CLI infrastructure",
"complexityScore": 3,
"recommendedSubtasks": 2,
"expansionPrompt": "Complete CLI integration: 1) Add AutopilotCommand to command-registry.ts following existing patterns and update command metadata, 2) Test command registration and help system integration with proper cleanup and error handling",
"reasoning": "Low-medium complexity. The command-registry.ts provides a clear pattern to follow. Main work is registration and ensuring proper integration with existing CLI infrastructure. Well-established patterns reduce complexity."
},
{
"taskId": 10,
"taskTitle": "Add comprehensive error handling and edge cases",
"complexityScore": 7,
"recommendedSubtasks": 5,
"expansionPrompt": "Implement error handling: 1) Add missing task and invalid task structure error handling with helpful messages, 2) Handle git state errors (dirty working tree, missing tools), 3) Add dependency validation errors (circular, invalid references), 4) Implement missing tool detection with installation guidance, 5) Create user-friendly error messages following existing CLI patterns",
"reasoning": "High complexity due to comprehensive error scenarios. Each component (preflight, task loading, dependency resolution) has multiple failure modes that need proper handling. Providing helpful error messages and recovery suggestions adds complexity."
}
]
}
```
--------------------------------------------------------------------------------
/src/progress/base-progress-tracker.js:
--------------------------------------------------------------------------------
```javascript
import { newMultiBar } from './cli-progress-factory.js';
/**
* Base class for progress trackers, handling common logic for time, tokens, estimation, and multibar management.
*/
export class BaseProgressTracker {
constructor(options = {}) {
this.numUnits = options.numUnits || 1;
this.unitName = options.unitName || 'unit'; // e.g., 'task', 'subtask'
this.startTime = null;
this.completedUnits = 0;
this.tokensIn = 0;
this.tokensOut = 0;
this.isEstimate = true; // For token display
// Time estimation properties
this.bestAvgTimePerUnit = null;
this.lastEstimateTime = null;
this.lastEstimateSeconds = 0;
// UI components
this.multibar = null;
this.timeTokensBar = null;
this.progressBar = null;
this._timerInterval = null;
// State flags
this.isStarted = false;
this.isFinished = false;
// Allow subclasses to define custom properties
this._initializeCustomProperties(options);
}
/**
* Protected method for subclasses to initialize custom properties.
* @protected
*/
_initializeCustomProperties(options) {
// Subclasses can override this
}
/**
* Get the pluralized form of the unit name for safe property keys.
* @returns {string} Pluralized unit name
*/
get unitNamePlural() {
return `${this.unitName}s`;
}
start() {
if (this.isStarted || this.isFinished) return;
this.isStarted = true;
this.startTime = Date.now();
this.multibar = newMultiBar();
// Create time/tokens bar using subclass-provided format
this.timeTokensBar = this.multibar.create(
1,
0,
{},
{
format: this._getTimeTokensBarFormat(),
barsize: 1,
hideCursor: true,
clearOnComplete: false
}
);
// Create main progress bar using subclass-provided format
this.progressBar = this.multibar.create(
this.numUnits,
0,
{},
{
format: this._getProgressBarFormat(),
barCompleteChar: '\u2588',
barIncompleteChar: '\u2591'
}
);
this._updateTimeTokensBar();
this.progressBar.update(0, { [this.unitNamePlural]: `0/${this.numUnits}` });
// Start timer
this._timerInterval = setInterval(() => this._updateTimeTokensBar(), 1000);
// Allow subclasses to add custom bars or setup
this._setupCustomUI();
}
/**
* Protected method for subclasses to add custom UI elements after start.
* @protected
*/
_setupCustomUI() {
// Subclasses can override this
}
/**
* Protected method to get the format for the time/tokens bar.
* @protected
* @returns {string} Format string for the time/tokens bar.
*/
_getTimeTokensBarFormat() {
return `{clock} {elapsed} | Tokens (I/O): {in}/{out} | Est: {remaining}`;
}
/**
* Protected method to get the format for the main progress bar.
* @protected
* @returns {string} Format string for the progress bar.
*/
_getProgressBarFormat() {
return `${this.unitName.charAt(0).toUpperCase() + this.unitName.slice(1)}s {${this.unitNamePlural}} |{bar}| {percentage}%`;
}
updateTokens(tokensIn, tokensOut, isEstimate = false) {
this.tokensIn = tokensIn || 0;
this.tokensOut = tokensOut || 0;
this.isEstimate = isEstimate;
this._updateTimeTokensBar();
}
_updateTimeTokensBar() {
if (!this.timeTokensBar || this.isFinished) return;
const elapsed = this._formatElapsedTime();
const remaining = this._estimateRemainingTime();
const tokensLabel = this.isEstimate ? '~ Tokens (I/O)' : 'Tokens (I/O)';
this.timeTokensBar.update(1, {
clock: '⏱️',
elapsed,
in: this.tokensIn,
out: this.tokensOut,
remaining,
tokensLabel,
// Subclasses can add more payload here via override
...this._getCustomTimeTokensPayload()
});
}
/**
* Protected method for subclasses to provide custom payload for time/tokens bar.
* @protected
* @returns {Object} Custom payload object.
*/
_getCustomTimeTokensPayload() {
return {};
}
_formatElapsedTime() {
if (!this.startTime) return '0m 00s';
const seconds = Math.floor((Date.now() - this.startTime) / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}m ${remainingSeconds.toString().padStart(2, '0')}s`;
}
_estimateRemainingTime() {
const progress = this._getProgressFraction();
if (progress >= 1) return '~0s';
const now = Date.now();
const elapsed = (now - this.startTime) / 1000;
if (progress === 0) return '~calculating...';
const avgTimePerUnit = elapsed / progress;
if (
this.bestAvgTimePerUnit === null ||
avgTimePerUnit < this.bestAvgTimePerUnit
) {
this.bestAvgTimePerUnit = avgTimePerUnit;
}
const remainingUnits = this.numUnits * (1 - progress);
let estimatedSeconds = Math.ceil(remainingUnits * this.bestAvgTimePerUnit);
// Stabilization logic
if (this.lastEstimateTime) {
const elapsedSinceEstimate = Math.floor(
(now - this.lastEstimateTime) / 1000
);
const countdownSeconds = Math.max(
0,
this.lastEstimateSeconds - elapsedSinceEstimate
);
if (countdownSeconds === 0) return '~0s';
estimatedSeconds = Math.min(estimatedSeconds, countdownSeconds);
}
this.lastEstimateTime = now;
this.lastEstimateSeconds = estimatedSeconds;
return `~${this._formatDuration(estimatedSeconds)}`;
}
/**
* Protected method for subclasses to calculate current progress fraction (0-1).
* Defaults to simple completedUnits / numUnits.
* @protected
* @returns {number} Progress fraction (can be fractional for subtasks).
*/
_getProgressFraction() {
return this.completedUnits / this.numUnits;
}
_formatDuration(seconds) {
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) {
return remainingSeconds > 0
? `${minutes}m ${remainingSeconds}s`
: `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m`;
}
getElapsedTime() {
return this.startTime ? Date.now() - this.startTime : 0;
}
stop() {
if (this.isFinished) return;
this.isFinished = true;
if (this._timerInterval) {
clearInterval(this._timerInterval);
this._timerInterval = null;
}
if (this.multibar) {
this._updateTimeTokensBar();
this.multibar.stop();
}
// Ensure cleanup is called to prevent memory leaks
this.cleanup();
}
getSummary() {
return {
completedUnits: this.completedUnits,
elapsedTime: this.getElapsedTime()
// Subclasses should extend this
};
}
/**
* Cleanup method to ensure proper resource disposal and prevent memory leaks.
* Should be called when the progress tracker is no longer needed.
*/
cleanup() {
// Stop any active timers
if (this._timerInterval) {
clearInterval(this._timerInterval);
this._timerInterval = null;
}
// Stop and clear multibar
if (this.multibar) {
try {
this.multibar.stop();
} catch (error) {
// Ignore errors during cleanup
}
this.multibar = null;
}
// Clear progress bar references
this.timeTokensBar = null;
this.progressBar = null;
// Reset state
this.isStarted = false;
this.isFinished = true;
// Allow subclasses to perform custom cleanup
this._performCustomCleanup();
}
/**
* Protected method for subclasses to perform custom cleanup.
* @protected
*/
_performCustomCleanup() {
// Subclasses can override this
}
}
```
--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/expand-task.js:
--------------------------------------------------------------------------------
```javascript
/**
* expand-task.js
* Direct function implementation for expanding a task into subtasks
*/
import expandTask from '../../../../scripts/modules/task-manager/expand-task.js';
import {
readJSON,
writeJSON,
enableSilentMode,
disableSilentMode,
isSilentMode
} from '../../../../scripts/modules/utils.js';
import path from 'path';
import fs from 'fs';
import { createLogWrapper } from '../../tools/utils.js';
/**
* Direct function wrapper for expanding a task into subtasks with error handling.
*
* @param {Object} args - Command arguments
* @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
* @param {string} args.id - The ID of the task to expand.
* @param {number|string} [args.num] - Number of subtasks to generate.
* @param {boolean} [args.research] - Enable research role for subtask generation.
* @param {string} [args.prompt] - Additional context to guide subtask generation.
* @param {boolean} [args.force] - Force expansion even if subtasks exist.
* @param {string} [args.projectRoot] - Project root directory.
* @param {string} [args.tag] - Tag for the task
* @param {Object} log - Logger object
* @param {Object} context - Context object containing session
* @param {Object} [context.session] - MCP Session object
* @returns {Promise<Object>} - Task expansion result { success: boolean, data?: any, error?: { code: string, message: string } }
*/
export async function expandTaskDirect(args, log, context = {}) {
const { session } = context; // Extract session
// Destructure expected args, including projectRoot
const {
tasksJsonPath,
id,
num,
research,
prompt,
force,
projectRoot,
tag,
complexityReportPath
} = args;
// Log session root data for debugging
log.info(
`Session data in expandTaskDirect: ${JSON.stringify({
hasSession: !!session,
sessionKeys: session ? Object.keys(session) : [],
roots: session?.roots,
rootsStr: JSON.stringify(session?.roots)
})}`
);
// Check if tasksJsonPath was provided
if (!tasksJsonPath) {
log.error('expandTaskDirect called without tasksJsonPath');
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'tasksJsonPath is required'
}
};
}
// Use provided path
const tasksPath = tasksJsonPath;
log.info(`[expandTaskDirect] Using tasksPath: ${tasksPath}`);
// Validate task ID
const taskId = id ? parseInt(id, 10) : null;
if (!taskId) {
log.error('Task ID is required');
return {
success: false,
error: {
code: 'INPUT_VALIDATION_ERROR',
message: 'Task ID is required'
}
};
}
// Process other parameters
const numSubtasks = num ? parseInt(num, 10) : undefined;
const useResearch = research === true;
const additionalContext = prompt || '';
const forceFlag = force === true;
try {
log.info(
`[expandTaskDirect] Expanding task ${taskId} into ${numSubtasks || 'default'} subtasks. Research: ${useResearch}, Force: ${forceFlag}`
);
// Read tasks data
log.info(`[expandTaskDirect] Attempting to read JSON from: ${tasksPath}`);
const data = readJSON(tasksPath, projectRoot);
log.info(
`[expandTaskDirect] Result of readJSON: ${data ? 'Data read successfully' : 'readJSON returned null or undefined'}`
);
if (!data || !data.tasks) {
log.error(
`[expandTaskDirect] readJSON failed or returned invalid data for path: ${tasksPath}`
);
return {
success: false,
error: {
code: 'INVALID_TASKS_FILE',
message: `No valid tasks found in ${tasksPath}. readJSON returned: ${JSON.stringify(data)}`
}
};
}
// Find the specific task
log.info(`[expandTaskDirect] Searching for task ID ${taskId} in data`);
const task = data.tasks.find((t) => t.id === taskId);
log.info(`[expandTaskDirect] Task found: ${task ? 'Yes' : 'No'}`);
if (!task) {
return {
success: false,
error: {
code: 'TASK_NOT_FOUND',
message: `Task with ID ${taskId} not found`
}
};
}
// Check if task is completed
if (task.status === 'done' || task.status === 'completed') {
return {
success: false,
error: {
code: 'TASK_COMPLETED',
message: `Task ${taskId} is already marked as ${task.status} and cannot be expanded`
}
};
}
// Check for existing subtasks and force flag
const hasExistingSubtasks = task.subtasks && task.subtasks.length > 0;
if (hasExistingSubtasks && !forceFlag) {
log.info(
`Task ${taskId} already has ${task.subtasks.length} subtasks. Use --force to overwrite.`
);
return {
success: true,
data: {
message: `Task ${taskId} already has subtasks. Expansion skipped.`,
task,
subtasksAdded: 0,
hasExistingSubtasks
}
};
}
// If force flag is set, clear existing subtasks
if (hasExistingSubtasks && forceFlag) {
log.info(
`Force flag set. Clearing existing subtasks for task ${taskId}.`
);
task.subtasks = [];
}
// Keep a copy of the task before modification
const originalTask = JSON.parse(JSON.stringify(task));
// Tracking subtasks count before expansion
const subtasksCountBefore = task.subtasks ? task.subtasks.length : 0;
// Directly modify the data instead of calling the CLI function
if (!task.subtasks) {
task.subtasks = [];
}
// Save tasks.json with potentially empty subtasks array and proper context
writeJSON(tasksPath, data, projectRoot, tag);
// Create logger wrapper using the utility
const mcpLog = createLogWrapper(log);
let wasSilent; // Declare wasSilent outside the try block
// Process the request
try {
// Enable silent mode to prevent console logs from interfering with JSON response
wasSilent = isSilentMode(); // Assign inside the try block
if (!wasSilent) enableSilentMode();
// Call the core expandTask function with the wrapped logger and projectRoot
const coreResult = await expandTask(
tasksPath,
taskId,
numSubtasks,
useResearch,
additionalContext,
{
complexityReportPath,
mcpLog,
session,
projectRoot,
commandName: 'expand-task',
outputType: 'mcp',
tag
},
forceFlag
);
// Restore normal logging
if (!wasSilent && isSilentMode()) disableSilentMode();
// Read the updated data
const updatedData = readJSON(tasksPath, projectRoot);
const updatedTask = updatedData.tasks.find((t) => t.id === taskId);
// Calculate how many subtasks were added
const subtasksAdded = updatedTask.subtasks
? updatedTask.subtasks.length - subtasksCountBefore
: 0;
// Return the result, including telemetryData
log.info(
`Successfully expanded task ${taskId} with ${subtasksAdded} new subtasks`
);
return {
success: true,
data: {
task: coreResult.task,
subtasksAdded,
hasExistingSubtasks,
telemetryData: coreResult.telemetryData,
tagInfo: coreResult.tagInfo
}
};
} catch (error) {
// Make sure to restore normal logging even if there's an error
if (!wasSilent && isSilentMode()) disableSilentMode();
log.error(`Error expanding task: ${error.message}`);
return {
success: false,
error: {
code: 'CORE_FUNCTION_ERROR',
message: error.message || 'Failed to expand task'
}
};
}
} catch (error) {
log.error(`Error expanding task: ${error.message}`);
return {
success: false,
error: {
code: 'CORE_FUNCTION_ERROR',
message: error.message || 'Failed to expand task'
}
};
}
}
```
--------------------------------------------------------------------------------
/apps/extension/src/webview/index.css:
--------------------------------------------------------------------------------
```css
@import "tailwindcss";
/* shadcn/ui CSS variables */
@theme {
/* VS Code CSS variables will be injected here */
/* color-scheme: var(--vscode-theme-kind, light); */
/* shadcn/ui variables - adapted for VS Code */
--color-background: var(--vscode-editor-background);
--color-sidebar-background: var(--vscode-sideBar-background);
--color-foreground: var(--vscode-foreground);
--color-card: var(--vscode-editor-background);
--color-card-foreground: var(--vscode-foreground);
--color-popover: var(--vscode-editor-background);
--color-popover-foreground: var(--vscode-foreground);
--color-primary: var(--vscode-button-background);
--color-primary-foreground: var(--vscode-button-foreground);
--color-secondary: var(--vscode-button-secondaryBackground);
--color-secondary-foreground: var(--vscode-button-secondaryForeground);
--color-widget-background: var(--vscode-editorWidget-background);
--color-widget-border: var(--vscode-editorWidget-border);
--color-code-snippet-background: var(--vscode-textPreformat-background);
--color-code-snippet-text: var(--vscode-textPreformat-foreground);
--font-editor-font: var(--vscode-editor-font-family);
--font-editor-size: var(--vscode-editor-font-size);
--color-input-background: var(--vscode-input-background);
--color-input-foreground: var(--vscode-input-foreground);
--color-accent: var(--vscode-focusBorder);
--color-accent-foreground: var(--vscode-foreground);
--color-destructive: var(--vscode-errorForeground);
--color-destructive-foreground: var(--vscode-foreground);
--color-border: var(--vscode-panel-border);
--color-ring: var(--vscode-focusBorder);
--color-link: var(--vscode-editorLink-foreground);
--color-link-hover: var(--vscode-editorLink-activeForeground);
--color-textSeparator-foreground: var(--vscode-textSeparator-foreground);
--radius: 0.5rem;
/* VS Code specific color mappings for Tailwind utilities */
--color-vscode-foreground: var(--vscode-foreground);
--color-vscode-button-background: var(--vscode-button-background);
--color-vscode-button-foreground: var(--vscode-button-foreground);
--color-vscode-button-hoverBackground: var(--vscode-button-hoverBackground);
--color-vscode-editor-background: var(--vscode-editor-background);
--color-vscode-input-background: var(--vscode-input-background);
--color-vscode-input-foreground: var(--vscode-input-foreground);
--color-vscode-dropdown-background: var(--vscode-dropdown-background);
--color-vscode-dropdown-foreground: var(--vscode-dropdown-foreground);
--color-vscode-dropdown-border: var(--vscode-dropdown-border);
--color-vscode-focusBorder: var(--vscode-focusBorder);
--color-vscode-panel-border: var(--vscode-panel-border);
--color-vscode-sideBar-background: var(--vscode-sideBar-background);
--color-vscode-sideBar-foreground: var(--vscode-sideBar-foreground);
--color-vscode-sideBarTitle-foreground: var(--vscode-sideBarTitle-foreground);
--color-vscode-testing-iconPassed: var(--vscode-testing-iconPassed);
--color-vscode-testing-iconFailed: var(--vscode-testing-iconFailed);
--color-vscode-errorForeground: var(--vscode-errorForeground);
--color-vscode-editorWidget-background: var(--vscode-editorWidget-background);
--color-vscode-editorWidget-border: var(--vscode-editorWidget-border);
--color-vscode-list-hoverBackground: var(--vscode-list-hoverBackground);
--color-vscode-list-activeSelectionBackground: var(
--vscode-list-activeSelectionBackground
);
--color-vscode-list-activeSelectionForeground: var(
--vscode-list-activeSelectionForeground
);
--color-vscode-badge-background: var(--vscode-badge-background);
--color-vscode-badge-foreground: var(--vscode-badge-foreground);
--color-vscode-textLink-foreground: var(--vscode-textLink-foreground);
--color-vscode-textLink-activeForeground: var(
--vscode-textLink-activeForeground
);
--color-vscode-icon-foreground: var(--vscode-icon-foreground);
--color-vscode-descriptionForeground: var(--vscode-descriptionForeground);
--color-vscode-disabledForeground: var(--vscode-disabledForeground);
}
/* Reset body to match VS Code styles instead of Tailwind defaults */
@layer base {
html,
body {
height: 100%;
margin: 0 !important;
padding: 0 !important;
overflow: hidden;
}
body {
background-color: var(--vscode-editor-background) !important;
color: var(--vscode-foreground) !important;
font-family: var(--vscode-font-family) !important;
font-size: var(--vscode-font-size) !important;
font-weight: var(--vscode-font-weight) !important;
line-height: 1.4 !important;
}
/* Ensure root container takes full space */
#root {
height: 100vh;
width: 100vw;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Override any conflicting Tailwind defaults for VS Code integration */
* {
box-sizing: border-box;
}
/* Ensure buttons and inputs use VS Code styling */
button,
input,
select,
textarea {
font-family: inherit;
}
}
/* Enhanced scrollbar styling for Kanban board */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: var(
--vscode-scrollbarSlider-hoverBackground,
rgba(255, 255, 255, 0.2)
);
border-radius: 4px;
border: 1px solid transparent;
background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover {
background: var(
--vscode-scrollbarSlider-activeBackground,
rgba(255, 255, 255, 0.3)
);
}
::-webkit-scrollbar-corner {
background: var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
}
/* Kanban specific styles */
@layer components {
.kanban-container {
scrollbar-gutter: stable;
}
/* Smooth scrolling for better UX */
.kanban-container {
scroll-behavior: smooth;
}
/* Ensure proper touch scrolling on mobile */
.kanban-container {
-webkit-overflow-scrolling: touch;
}
/* Add subtle shadow for depth */
.kanban-column {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* Enhanced scrolling for column content areas */
.kanban-column > div[style*="overflow-y"] {
scrollbar-width: thin;
scrollbar-color: var(
--vscode-scrollbarSlider-hoverBackground,
rgba(255, 255, 255, 0.2)
)
var(--vscode-scrollbarSlider-background, rgba(255, 255, 255, 0.1));
}
/* Card hover effects */
.kanban-card {
transition: all 0.2s ease-in-out;
}
.kanban-card:hover {
transform: translateY(-1px);
}
/* Focus indicators for accessibility */
.kanban-card:focus-visible {
outline: 2px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
}
/* Line clamp utility for text truncation */
@layer utilities {
.line-clamp-2 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.line-clamp-3 {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
/* Custom scrollbar utilities */
.scrollbar-thin {
scrollbar-width: thin;
}
.scrollbar-track-transparent {
scrollbar-color: var(
--vscode-scrollbarSlider-hoverBackground,
rgba(255, 255, 255, 0.2)
)
transparent;
}
}
/* Dark mode adjustments */
@media (prefers-color-scheme: dark) {
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.25);
}
}
```
--------------------------------------------------------------------------------
/apps/cli/src/command-registry.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview Centralized Command Registry
* Provides a single location for registering all CLI commands
*/
import type { Command } from 'commander';
import { AuthCommand } from './commands/auth.command.js';
import { AutopilotCommand } from './commands/autopilot/index.js';
import { BriefsCommand } from './commands/briefs.command.js';
import { ContextCommand } from './commands/context.command.js';
import { ExportCommand } from './commands/export.command.js';
// Import all commands
import { ListTasksCommand } from './commands/list.command.js';
import { NextCommand } from './commands/next.command.js';
import { SetStatusCommand } from './commands/set-status.command.js';
import { ShowCommand } from './commands/show.command.js';
import { StartCommand } from './commands/start.command.js';
import { TagsCommand } from './commands/tags.command.js';
/**
* Command metadata for registration
*/
export interface CommandMetadata {
name: string;
description: string;
commandClass: typeof Command;
category?: 'task' | 'auth' | 'utility' | 'development';
}
/**
* Registry of all available commands
*/
export class CommandRegistry {
/**
* All available commands with their metadata
*/
private static commands: CommandMetadata[] = [
// Task Management Commands
{
name: 'list',
description: 'List all tasks with filtering and status overview',
commandClass: ListTasksCommand as any,
category: 'task'
},
{
name: 'show',
description: 'Display detailed information about a specific task',
commandClass: ShowCommand as any,
category: 'task'
},
{
name: 'next',
description: 'Find the next available task to work on',
commandClass: NextCommand as any,
category: 'task'
},
{
name: 'start',
description: 'Start working on a task with claude-code',
commandClass: StartCommand as any,
category: 'task'
},
{
name: 'set-status',
description: 'Update the status of one or more tasks',
commandClass: SetStatusCommand as any,
category: 'task'
},
{
name: 'export',
description: 'Export tasks to external systems',
commandClass: ExportCommand as any,
category: 'task'
},
{
name: 'autopilot',
description:
'AI agent orchestration for TDD workflow (start, resume, next, complete, commit, status, abort)',
commandClass: AutopilotCommand as any,
category: 'development'
},
// Authentication & Context Commands
{
name: 'auth',
description: 'Manage authentication with tryhamster.com',
commandClass: AuthCommand as any,
category: 'auth'
},
{
name: 'context',
description: 'Manage workspace context (organization/brief)',
commandClass: ContextCommand as any,
category: 'auth'
},
{
name: 'tags',
description: 'Manage tags for task organization',
commandClass: TagsCommand as any,
category: 'task'
},
{
name: 'briefs',
description: 'Manage briefs (Hamster only)',
commandClass: BriefsCommand as any,
category: 'task'
}
];
/**
* Register all commands on a program instance
* @param program - Commander program to register commands on
*/
static registerAll(program: Command): void {
for (const cmd of this.commands) {
this.registerCommand(program, cmd);
}
}
/**
* Register specific commands by category
* @param program - Commander program to register commands on
* @param category - Category of commands to register
*/
static registerByCategory(
program: Command,
category: 'task' | 'auth' | 'utility' | 'development'
): void {
const categoryCommands = this.commands.filter(
(cmd) => cmd.category === category
);
for (const cmd of categoryCommands) {
this.registerCommand(program, cmd);
}
}
/**
* Register a single command by name
* @param program - Commander program to register the command on
* @param name - Name of the command to register
*/
static registerByName(program: Command, name: string): void {
const cmd = this.commands.find((c) => c.name === name);
if (cmd) {
this.registerCommand(program, cmd);
} else {
throw new Error(`Command '${name}' not found in registry`);
}
}
/**
* Register a single command
* @param program - Commander program to register the command on
* @param metadata - Command metadata
*/
private static registerCommand(
program: Command,
metadata: CommandMetadata
): void {
const CommandClass = metadata.commandClass as any;
// Use the static registration method that all commands have
if (CommandClass.registerOn) {
CommandClass.registerOn(program);
} else if (CommandClass.register) {
CommandClass.register(program);
} else {
// Fallback to creating instance and adding
const instance = new CommandClass();
program.addCommand(instance);
}
}
/**
* Get all registered command names
*/
static getCommandNames(): string[] {
return this.commands.map((cmd) => cmd.name);
}
/**
* Get commands by category
*/
static getCommandsByCategory(
category: 'task' | 'auth' | 'utility' | 'development'
): CommandMetadata[] {
return this.commands.filter((cmd) => cmd.category === category);
}
/**
* Add a new command to the registry
* @param metadata - Command metadata to add
*/
static addCommand(metadata: CommandMetadata): void {
// Check if command already exists
if (this.commands.some((cmd) => cmd.name === metadata.name)) {
throw new Error(`Command '${metadata.name}' already exists in registry`);
}
this.commands.push(metadata);
}
/**
* Remove a command from the registry
* @param name - Name of the command to remove
*/
static removeCommand(name: string): boolean {
const index = this.commands.findIndex((cmd) => cmd.name === name);
if (index >= 0) {
this.commands.splice(index, 1);
return true;
}
return false;
}
/**
* Get command metadata by name
* @param name - Name of the command
*/
static getCommand(name: string): CommandMetadata | undefined {
return this.commands.find((cmd) => cmd.name === name);
}
/**
* Check if a command exists
* @param name - Name of the command
*/
static hasCommand(name: string): boolean {
return this.commands.some((cmd) => cmd.name === name);
}
/**
* Get a formatted list of all commands for display
*/
static getFormattedCommandList(): string {
const categories = {
task: 'Task Management',
auth: 'Authentication & Context',
utility: 'Utilities',
development: 'Development'
};
let output = '';
for (const [category, title] of Object.entries(categories)) {
const cmds = this.getCommandsByCategory(
category as keyof typeof categories
);
if (cmds.length > 0) {
output += `\n${title}:\n`;
for (const cmd of cmds) {
output += ` ${cmd.name.padEnd(20)} ${cmd.description}\n`;
}
}
}
return output;
}
}
/**
* Convenience function to register all CLI commands
* @param program - Commander program instance
*/
export function registerAllCommands(program: Command): void {
CommandRegistry.registerAll(program);
}
/**
* Convenience function to register commands by category
* @param program - Commander program instance
* @param category - Category to register
*/
export function registerCommandsByCategory(
program: Command,
category: 'task' | 'auth' | 'utility' | 'development'
): void {
CommandRegistry.registerByCategory(program, category);
}
// Export the registry for direct access if needed
export default CommandRegistry;
```
--------------------------------------------------------------------------------
/src/ui/indicators.js:
--------------------------------------------------------------------------------
```javascript
/**
* indicators.js
* UI functions for displaying priority and complexity indicators in different contexts
*/
import chalk from 'chalk';
import { TASK_PRIORITY_OPTIONS } from '../constants/task-priority.js';
// Extract priority values for cleaner object keys
const [HIGH, MEDIUM, LOW] = TASK_PRIORITY_OPTIONS;
// Cache for generated indicators
const INDICATOR_CACHE = new Map();
/**
* Base configuration for indicator systems
*/
class IndicatorConfig {
constructor(name, levels, colors, thresholds = null) {
this.name = name;
this.levels = levels;
this.colors = colors;
this.thresholds = thresholds;
}
getColor(level) {
return this.colors[level] || chalk.gray;
}
getLevelFromScore(score) {
if (!this.thresholds) {
throw new Error(`${this.name} does not support score-based levels`);
}
if (score >= 7) return this.levels[0]; // high
if (score <= 3) return this.levels[2]; // low
return this.levels[1]; // medium
}
}
/**
* Visual style definitions
*/
const VISUAL_STYLES = {
cli: {
filled: '●', // ●
empty: '○' // ○
},
statusBar: {
high: '⋮', // ⋮
medium: ':', // :
low: '.' // .
},
mcp: {
high: '🔴', // 🔴
medium: '🟠', // 🟠
low: '🟢' // 🟢
}
};
/**
* Priority configuration
*/
const PRIORITY_CONFIG = new IndicatorConfig('priority', [HIGH, MEDIUM, LOW], {
[HIGH]: chalk.hex('#CC0000'),
[MEDIUM]: chalk.hex('#FF8800'),
[LOW]: chalk.yellow
});
/**
* Generates CLI indicator with intensity
*/
function generateCliIndicator(intensity, color) {
const filled = VISUAL_STYLES.cli.filled;
const empty = VISUAL_STYLES.cli.empty;
let indicator = '';
for (let i = 0; i < 3; i++) {
if (i < intensity) {
indicator += color(filled);
} else {
indicator += chalk.white(empty);
}
}
return indicator;
}
/**
* Get intensity level from priority/complexity level
*/
function getIntensityFromLevel(level, levels) {
const index = levels.indexOf(level);
return 3 - index; // high=3, medium=2, low=1
}
/**
* Generic cached indicator getter
* @param {string} cacheKey - Cache key for the indicators
* @param {Function} generator - Function to generate the indicators
* @returns {Object} Cached or newly generated indicators
*/
function getCachedIndicators(cacheKey, generator) {
if (INDICATOR_CACHE.has(cacheKey)) {
return INDICATOR_CACHE.get(cacheKey);
}
const indicators = generator();
INDICATOR_CACHE.set(cacheKey, indicators);
return indicators;
}
/**
* Get priority indicators for MCP context (single emojis)
* @returns {Object} Priority to emoji mapping
*/
export function getMcpPriorityIndicators() {
return getCachedIndicators('mcp-priority-all', () => ({
[HIGH]: VISUAL_STYLES.mcp.high,
[MEDIUM]: VISUAL_STYLES.mcp.medium,
[LOW]: VISUAL_STYLES.mcp.low
}));
}
/**
* Get priority indicators for CLI context (colored dots with visual hierarchy)
* @returns {Object} Priority to colored dot string mapping
*/
export function getCliPriorityIndicators() {
return getCachedIndicators('cli-priority-all', () => {
const indicators = {};
PRIORITY_CONFIG.levels.forEach((level) => {
const intensity = getIntensityFromLevel(level, PRIORITY_CONFIG.levels);
const color = PRIORITY_CONFIG.getColor(level);
indicators[level] = generateCliIndicator(intensity, color);
});
return indicators;
});
}
/**
* Get priority indicators for status bars (simplified single character versions)
* @returns {Object} Priority to single character indicator mapping
*/
export function getStatusBarPriorityIndicators() {
return getCachedIndicators('statusbar-priority-all', () => {
const indicators = {};
PRIORITY_CONFIG.levels.forEach((level, index) => {
const style =
index === 0
? VISUAL_STYLES.statusBar.high
: index === 1
? VISUAL_STYLES.statusBar.medium
: VISUAL_STYLES.statusBar.low;
const color = PRIORITY_CONFIG.getColor(level);
indicators[level] = color(style);
});
return indicators;
});
}
/**
* Get priority colors for consistent styling
* @returns {Object} Priority to chalk color function mapping
*/
export function getPriorityColors() {
return {
[HIGH]: PRIORITY_CONFIG.colors[HIGH],
[MEDIUM]: PRIORITY_CONFIG.colors[MEDIUM],
[LOW]: PRIORITY_CONFIG.colors[LOW]
};
}
/**
* Get priority indicators based on context
* @param {boolean} isMcp - Whether this is for MCP context (true) or CLI context (false)
* @returns {Object} Priority to indicator mapping
*/
export function getPriorityIndicators(isMcp = false) {
return isMcp ? getMcpPriorityIndicators() : getCliPriorityIndicators();
}
/**
* Get a specific priority indicator
* @param {string} priority - The priority level ('high', 'medium', 'low')
* @param {boolean} isMcp - Whether this is for MCP context
* @returns {string} The indicator string for the priority
*/
export function getPriorityIndicator(priority, isMcp = false) {
const indicators = getPriorityIndicators(isMcp);
return indicators[priority] || indicators[MEDIUM];
}
// ============================================================================
// Complexity Indicators
// ============================================================================
/**
* Complexity configuration
*/
const COMPLEXITY_CONFIG = new IndicatorConfig(
'complexity',
['high', 'medium', 'low'],
{
high: chalk.hex('#CC0000'),
medium: chalk.hex('#FF8800'),
low: chalk.green
},
{
high: (score) => score >= 7,
medium: (score) => score >= 4 && score <= 6,
low: (score) => score <= 3
}
);
/**
* Get complexity indicators for CLI context (colored dots with visual hierarchy)
* Complexity scores: 1-3 (low), 4-6 (medium), 7-10 (high)
* @returns {Object} Complexity level to colored dot string mapping
*/
export function getCliComplexityIndicators() {
return getCachedIndicators('cli-complexity-all', () => {
const indicators = {};
COMPLEXITY_CONFIG.levels.forEach((level) => {
const intensity = getIntensityFromLevel(level, COMPLEXITY_CONFIG.levels);
const color = COMPLEXITY_CONFIG.getColor(level);
indicators[level] = generateCliIndicator(intensity, color);
});
return indicators;
});
}
/**
* Get complexity indicators for status bars (simplified single character versions)
* @returns {Object} Complexity level to single character indicator mapping
*/
export function getStatusBarComplexityIndicators() {
return getCachedIndicators('statusbar-complexity-all', () => {
const indicators = {};
COMPLEXITY_CONFIG.levels.forEach((level, index) => {
const style =
index === 0
? VISUAL_STYLES.statusBar.high
: index === 1
? VISUAL_STYLES.statusBar.medium
: VISUAL_STYLES.statusBar.low;
const color = COMPLEXITY_CONFIG.getColor(level);
indicators[level] = color(style);
});
return indicators;
});
}
/**
* Get complexity colors for consistent styling
* @returns {Object} Complexity level to chalk color function mapping
*/
export function getComplexityColors() {
return { ...COMPLEXITY_CONFIG.colors };
}
/**
* Get a specific complexity indicator based on score
* @param {number} score - The complexity score (1-10)
* @param {boolean} statusBar - Whether to return status bar version (single char)
* @returns {string} The indicator string for the complexity level
*/
export function getComplexityIndicator(score, statusBar = false) {
const level = COMPLEXITY_CONFIG.getLevelFromScore(score);
const indicators = statusBar
? getStatusBarComplexityIndicators()
: getCliComplexityIndicators();
return indicators[level];
}
```
--------------------------------------------------------------------------------
/apps/cli/src/commands/set-status.command.ts:
--------------------------------------------------------------------------------
```typescript
/**
* @fileoverview SetStatusCommand using Commander's native class pattern
* Extends Commander.Command for better integration with the framework
*/
import { type TaskStatus, type TmCore, createTmCore } from '@tm/core';
import type { StorageType } from '@tm/core';
import boxen from 'boxen';
import chalk from 'chalk';
import { Command } from 'commander';
import { displayError } from '../utils/error-handler.js';
import { getProjectRoot } from '../utils/project-root.js';
/**
* Valid task status values for validation
*/
const VALID_TASK_STATUSES: TaskStatus[] = [
'pending',
'in-progress',
'done',
'deferred',
'cancelled',
'blocked',
'review'
];
/**
* Options interface for the set-status command
*/
export interface SetStatusCommandOptions {
id?: string;
status?: TaskStatus;
format?: 'text' | 'json';
silent?: boolean;
project?: string;
}
/**
* Result type from set-status command
*/
export interface SetStatusResult {
success: boolean;
updatedTasks: Array<{
taskId: string;
oldStatus: TaskStatus;
newStatus: TaskStatus;
}>;
storageType: Exclude<StorageType, 'auto'>;
}
/**
* SetStatusCommand extending Commander's Command class
* This is a thin presentation layer over @tm/core
*/
export class SetStatusCommand extends Command {
private tmCore?: TmCore;
private lastResult?: SetStatusResult;
constructor(name?: string) {
super(name || 'set-status');
// Configure the command
this.description('Update the status of one or more tasks')
.requiredOption(
'-i, --id <id>',
'Task ID(s) to update (comma-separated for multiple, supports subtasks like 5.2)'
)
.requiredOption(
'-s, --status <status>',
`New status (${VALID_TASK_STATUSES.join(', ')})`
)
.option('-f, --format <format>', 'Output format (text, json)', 'text')
.option('--silent', 'Suppress output (useful for programmatic usage)')
.option(
'-p, --project <path>',
'Project root directory (auto-detected if not provided)'
)
.action(async (options: SetStatusCommandOptions) => {
await this.executeCommand(options);
});
}
/**
* Execute the set-status command
*/
private async executeCommand(
options: SetStatusCommandOptions
): Promise<void> {
let hasError = false;
try {
// Validate required options
if (!options.id) {
console.error(chalk.red('Error: Task ID is required. Use -i or --id'));
process.exit(1);
}
if (!options.status) {
console.error(
chalk.red('Error: Status is required. Use -s or --status')
);
process.exit(1);
}
// Validate status
if (!VALID_TASK_STATUSES.includes(options.status)) {
console.error(
chalk.red(
`Error: Invalid status "${options.status}". Valid options: ${VALID_TASK_STATUSES.join(', ')}`
)
);
process.exit(1);
}
// Initialize TaskMaster core
this.tmCore = await createTmCore({
projectPath: getProjectRoot(options.project)
});
// Parse task IDs (handle comma-separated values)
const taskIds = options.id.split(',').map((id) => id.trim());
// Update each task
const updatedTasks: Array<{
taskId: string;
oldStatus: TaskStatus;
newStatus: TaskStatus;
}> = [];
for (const taskId of taskIds) {
try {
const result = await this.tmCore.tasks.updateStatus(
taskId,
options.status
);
updatedTasks.push({
taskId: result.taskId,
oldStatus: result.oldStatus,
newStatus: result.newStatus
});
} catch (error: any) {
hasError = true;
if (options.format === 'json') {
const errorMessage = error?.getSanitizedDetails
? error.getSanitizedDetails().message
: error instanceof Error
? error.message
: String(error);
console.log(
JSON.stringify({
success: false,
error: errorMessage,
taskId,
timestamp: new Date().toISOString()
})
);
} else if (!options.silent) {
// Show which task failed with context
console.error(chalk.red(`\nFailed to update task ${taskId}:`));
displayError(error, { skipExit: true });
}
// Don't exit here - let finally block clean up first
break;
}
}
// Store result for potential reuse
this.lastResult = {
success: true,
updatedTasks,
storageType: this.tmCore.tasks.getStorageType()
};
// Display results
this.displayResults(this.lastResult, options);
} catch (error: any) {
hasError = true;
if (options.format === 'json') {
const errorMessage =
error instanceof Error ? error.message : 'Unknown error occurred';
console.log(JSON.stringify({ success: false, error: errorMessage }));
} else if (!options.silent) {
displayError(error, { skipExit: true });
}
} finally {
// Clean up resources
if (this.tmCore) {
}
}
// Exit after cleanup completes
if (hasError) {
process.exit(1);
}
}
/**
* Display results based on format
*/
private displayResults(
result: SetStatusResult,
options: SetStatusCommandOptions
): void {
const format = options.format || 'text';
switch (format) {
case 'json':
console.log(JSON.stringify(result, null, 2));
break;
case 'text':
default:
if (!options.silent) {
this.displayTextResults(result);
}
break;
}
}
/**
* Display results in text format
*/
private displayTextResults(result: SetStatusResult): void {
if (result.updatedTasks.length === 1) {
// Single task update
const update = result.updatedTasks[0];
console.log(
boxen(
chalk.white.bold(`✅ Successfully updated task ${update.taskId}`) +
'\n\n' +
`${chalk.blue('From:')} ${this.getStatusDisplay(update.oldStatus)}\n` +
`${chalk.blue('To:')} ${this.getStatusDisplay(update.newStatus)}`,
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
} else {
// Multiple task updates
console.log(
boxen(
chalk.white.bold(
`✅ Successfully updated ${result.updatedTasks.length} tasks`
) +
'\n\n' +
result.updatedTasks
.map(
(update) =>
`${chalk.cyan(update.taskId)}: ${this.getStatusDisplay(update.oldStatus)} → ${this.getStatusDisplay(update.newStatus)}`
)
.join('\n'),
{
padding: 1,
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
}
}
/**
* Get colored status display
*/
private getStatusDisplay(status: TaskStatus): string {
const statusColors: Record<TaskStatus, (text: string) => string> = {
pending: chalk.yellow,
'in-progress': chalk.blue,
done: chalk.green,
deferred: chalk.gray,
cancelled: chalk.red,
blocked: chalk.red,
review: chalk.magenta,
completed: chalk.green
};
const colorFn = statusColors[status] || chalk.white;
return colorFn(status);
}
/**
* Get the last command result (useful for testing or chaining)
*/
getLastResult(): SetStatusResult | undefined {
return this.lastResult;
}
/**
* Register this command on an existing program
*/
static register(program: Command, name?: string): SetStatusCommand {
const setStatusCommand = new SetStatusCommand(name);
program.addCommand(setStatusCommand);
return setStatusCommand;
}
}
/**
* Factory function to create and configure the set-status command
*/
export function createSetStatusCommand(): SetStatusCommand {
return new SetStatusCommand();
}
```