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

# Directory Structure

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

# Files

--------------------------------------------------------------------------------
/.github/scripts/auto-close-duplicates.mjs:
--------------------------------------------------------------------------------

```
  1 | #!/usr/bin/env node
  2 | 
  3 | async function githubRequest(endpoint, token, method = 'GET', body) {
  4 | 	const response = await fetch(`https://api.github.com${endpoint}`, {
  5 | 		method,
  6 | 		headers: {
  7 | 			Authorization: `Bearer ${token}`,
  8 | 			Accept: 'application/vnd.github.v3+json',
  9 | 			'User-Agent': 'auto-close-duplicates-script',
 10 | 			...(body && { 'Content-Type': 'application/json' })
 11 | 		},
 12 | 		...(body && { body: JSON.stringify(body) })
 13 | 	});
 14 | 
 15 | 	if (!response.ok) {
 16 | 		throw new Error(
 17 | 			`GitHub API request failed: ${response.status} ${response.statusText}`
 18 | 		);
 19 | 	}
 20 | 
 21 | 	return response.json();
 22 | }
 23 | 
 24 | function extractDuplicateIssueNumber(commentBody) {
 25 | 	const match = commentBody.match(/#(\d+)/);
 26 | 	return match ? parseInt(match[1], 10) : null;
 27 | }
 28 | 
 29 | async function closeIssueAsDuplicate(
 30 | 	owner,
 31 | 	repo,
 32 | 	issueNumber,
 33 | 	duplicateOfNumber,
 34 | 	token
 35 | ) {
 36 | 	await githubRequest(
 37 | 		`/repos/${owner}/${repo}/issues/${issueNumber}`,
 38 | 		token,
 39 | 		'PATCH',
 40 | 		{
 41 | 			state: 'closed',
 42 | 			state_reason: 'not_planned',
 43 | 			labels: ['duplicate']
 44 | 		}
 45 | 	);
 46 | 
 47 | 	await githubRequest(
 48 | 		`/repos/${owner}/${repo}/issues/${issueNumber}/comments`,
 49 | 		token,
 50 | 		'POST',
 51 | 		{
 52 | 			body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}.
 53 | 
 54 | If this is incorrect, please re-open this issue or create a new one.
 55 | 
 56 | 🤖 Generated with [Task Master Bot]`
 57 | 		}
 58 | 	);
 59 | }
 60 | 
 61 | async function autoCloseDuplicates() {
 62 | 	console.log('[DEBUG] Starting auto-close duplicates script');
 63 | 
 64 | 	const token = process.env.GITHUB_TOKEN;
 65 | 	if (!token) {
 66 | 		throw new Error('GITHUB_TOKEN environment variable is required');
 67 | 	}
 68 | 	console.log('[DEBUG] GitHub token found');
 69 | 
 70 | 	const owner = process.env.GITHUB_REPOSITORY_OWNER || 'eyaltoledano';
 71 | 	const repo = process.env.GITHUB_REPOSITORY_NAME || 'claude-task-master';
 72 | 	console.log(`[DEBUG] Repository: ${owner}/${repo}`);
 73 | 
 74 | 	const threeDaysAgo = new Date();
 75 | 	threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
 76 | 	console.log(
 77 | 		`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`
 78 | 	);
 79 | 
 80 | 	console.log('[DEBUG] Fetching open issues created more than 3 days ago...');
 81 | 	const allIssues = [];
 82 | 	let page = 1;
 83 | 	const perPage = 100;
 84 | 
 85 | 	const MAX_PAGES = 50; // Increase limit for larger repos
 86 | 	let foundRecentIssue = false;
 87 | 
 88 | 	while (true) {
 89 | 		const pageIssues = await githubRequest(
 90 | 			`/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}&sort=created&direction=desc`,
 91 | 			token
 92 | 		);
 93 | 
 94 | 		if (pageIssues.length === 0) break;
 95 | 
 96 | 		// Filter for issues created more than 3 days ago
 97 | 		const oldEnoughIssues = pageIssues.filter(
 98 | 			(issue) => new Date(issue.created_at) <= threeDaysAgo
 99 | 		);
100 | 
101 | 		allIssues.push(...oldEnoughIssues);
102 | 
103 | 		// If all issues on this page are newer than 3 days, we can stop
104 | 		if (oldEnoughIssues.length === 0 && page === 1) {
105 | 			foundRecentIssue = true;
106 | 			break;
107 | 		}
108 | 
109 | 		// If we found some old issues but not all, continue to next page
110 | 		// as there might be more old issues
111 | 		page++;
112 | 
113 | 		// Safety limit to avoid infinite loops
114 | 		if (page > MAX_PAGES) {
115 | 			console.log(`[WARNING] Reached maximum page limit of ${MAX_PAGES}`);
116 | 			break;
117 | 		}
118 | 	}
119 | 
120 | 	const issues = allIssues;
121 | 	console.log(`[DEBUG] Found ${issues.length} open issues`);
122 | 
123 | 	let processedCount = 0;
124 | 	let candidateCount = 0;
125 | 
126 | 	for (const issue of issues) {
127 | 		processedCount++;
128 | 		console.log(
129 | 			`[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`
130 | 		);
131 | 
132 | 		console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`);
133 | 		const comments = await githubRequest(
134 | 			`/repos/${owner}/${repo}/issues/${issue.number}/comments`,
135 | 			token
136 | 		);
137 | 		console.log(
138 | 			`[DEBUG] Issue #${issue.number} has ${comments.length} comments`
139 | 		);
140 | 
141 | 		const dupeComments = comments.filter(
142 | 			(comment) =>
143 | 				comment.body.includes('Found') &&
144 | 				comment.body.includes('possible duplicate') &&
145 | 				comment.user.type === 'Bot'
146 | 		);
147 | 		console.log(
148 | 			`[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`
149 | 		);
150 | 
151 | 		if (dupeComments.length === 0) {
152 | 			console.log(
153 | 				`[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`
154 | 			);
155 | 			continue;
156 | 		}
157 | 
158 | 		const lastDupeComment = dupeComments[dupeComments.length - 1];
159 | 		const dupeCommentDate = new Date(lastDupeComment.created_at);
160 | 		console.log(
161 | 			`[DEBUG] Issue #${
162 | 				issue.number
163 | 			} - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`
164 | 		);
165 | 
166 | 		if (dupeCommentDate > threeDaysAgo) {
167 | 			console.log(
168 | 				`[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`
169 | 			);
170 | 			continue;
171 | 		}
172 | 		console.log(
173 | 			`[DEBUG] Issue #${
174 | 				issue.number
175 | 			} - duplicate comment is old enough (${Math.floor(
176 | 				(Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24)
177 | 			)} days)`
178 | 		);
179 | 
180 | 		const commentsAfterDupe = comments.filter(
181 | 			(comment) => new Date(comment.created_at) > dupeCommentDate
182 | 		);
183 | 		console.log(
184 | 			`[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`
185 | 		);
186 | 
187 | 		if (commentsAfterDupe.length > 0) {
188 | 			console.log(
189 | 				`[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`
190 | 			);
191 | 			continue;
192 | 		}
193 | 
194 | 		console.log(
195 | 			`[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`
196 | 		);
197 | 		const reactions = await githubRequest(
198 | 			`/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`,
199 | 			token
200 | 		);
201 | 		console.log(
202 | 			`[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`
203 | 		);
204 | 
205 | 		const authorThumbsDown = reactions.some(
206 | 			(reaction) =>
207 | 				reaction.user.id === issue.user.id && reaction.content === '-1'
208 | 		);
209 | 		console.log(
210 | 			`[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`
211 | 		);
212 | 
213 | 		if (authorThumbsDown) {
214 | 			console.log(
215 | 				`[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`
216 | 			);
217 | 			continue;
218 | 		}
219 | 
220 | 		const duplicateIssueNumber = extractDuplicateIssueNumber(
221 | 			lastDupeComment.body
222 | 		);
223 | 		if (!duplicateIssueNumber) {
224 | 			console.log(
225 | 				`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`
226 | 			);
227 | 			continue;
228 | 		}
229 | 
230 | 		candidateCount++;
231 | 		const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`;
232 | 
233 | 		try {
234 | 			console.log(
235 | 				`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`
236 | 			);
237 | 			await closeIssueAsDuplicate(
238 | 				owner,
239 | 				repo,
240 | 				issue.number,
241 | 				duplicateIssueNumber,
242 | 				token
243 | 			);
244 | 			console.log(
245 | 				`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`
246 | 			);
247 | 		} catch (error) {
248 | 			console.error(
249 | 				`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`
250 | 			);
251 | 		}
252 | 	}
253 | 
254 | 	console.log(
255 | 		`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`
256 | 	);
257 | }
258 | 
259 | autoCloseDuplicates().catch(console.error);
260 | 
```

--------------------------------------------------------------------------------
/apps/extension/src/webview/components/TaskEditModal.tsx:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Task Edit Modal Component
  3 |  */
  4 | 
  5 | import React, { useState, useEffect, useRef } from 'react';
  6 | import { Button } from '@/components/ui/button';
  7 | import { Label } from '@/components/ui/label';
  8 | import { Textarea } from '@/components/ui/textarea';
  9 | import {
 10 | 	DropdownMenu,
 11 | 	DropdownMenuContent,
 12 | 	DropdownMenuItem,
 13 | 	DropdownMenuTrigger
 14 | } from '@/components/ui/dropdown-menu';
 15 | import type { TaskMasterTask, TaskUpdates } from '../types';
 16 | 
 17 | interface TaskEditModalProps {
 18 | 	task: TaskMasterTask;
 19 | 	onSave: (taskId: string, updates: TaskUpdates) => Promise<void>;
 20 | 	onCancel: () => void;
 21 | }
 22 | 
 23 | export const TaskEditModal: React.FC<TaskEditModalProps> = ({
 24 | 	task,
 25 | 	onSave,
 26 | 	onCancel
 27 | }) => {
 28 | 	const [updates, setUpdates] = useState<TaskUpdates>({
 29 | 		title: task.title,
 30 | 		description: task.description || '',
 31 | 		details: task.details || '',
 32 | 		testStrategy: task.testStrategy || '',
 33 | 		priority: task.priority,
 34 | 		dependencies: task.dependencies || []
 35 | 	});
 36 | 	const [isSaving, setIsSaving] = useState(false);
 37 | 	const formRef = useRef<HTMLFormElement>(null);
 38 | 	const titleInputRef = useRef<HTMLInputElement>(null);
 39 | 
 40 | 	// Focus title input on mount
 41 | 	useEffect(() => {
 42 | 		titleInputRef.current?.focus();
 43 | 		titleInputRef.current?.select();
 44 | 	}, []);
 45 | 
 46 | 	const handleSubmit = async (e?: React.FormEvent) => {
 47 | 		e?.preventDefault();
 48 | 		setIsSaving(true);
 49 | 
 50 | 		try {
 51 | 			await onSave(task.id, updates);
 52 | 		} catch (error) {
 53 | 			console.error('Failed to save task:', error);
 54 | 		} finally {
 55 | 			setIsSaving(false);
 56 | 		}
 57 | 	};
 58 | 
 59 | 	const hasChanges = () => {
 60 | 		return (
 61 | 			updates.title !== task.title ||
 62 | 			updates.description !== (task.description || '') ||
 63 | 			updates.details !== (task.details || '') ||
 64 | 			updates.testStrategy !== (task.testStrategy || '') ||
 65 | 			updates.priority !== task.priority ||
 66 | 			JSON.stringify(updates.dependencies) !==
 67 | 				JSON.stringify(task.dependencies || [])
 68 | 		);
 69 | 	};
 70 | 
 71 | 	return (
 72 | 		<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
 73 | 			<div className="bg-vscode-editor-background border border-vscode-border rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
 74 | 				{/* Header */}
 75 | 				<div className="flex items-center justify-between p-4 border-b border-vscode-border">
 76 | 					<h2 className="text-lg font-semibold">Edit Task #{task.id}</h2>
 77 | 					<button
 78 | 						onClick={onCancel}
 79 | 						className="text-vscode-foreground/50 hover:text-vscode-foreground transition-colors"
 80 | 					>
 81 | 						<svg
 82 | 							className="w-5 h-5"
 83 | 							fill="none"
 84 | 							stroke="currentColor"
 85 | 							viewBox="0 0 24 24"
 86 | 						>
 87 | 							<path
 88 | 								strokeLinecap="round"
 89 | 								strokeLinejoin="round"
 90 | 								strokeWidth={2}
 91 | 								d="M6 18L18 6M6 6l12 12"
 92 | 							/>
 93 | 						</svg>
 94 | 					</button>
 95 | 				</div>
 96 | 
 97 | 				{/* Form */}
 98 | 				<form
 99 | 					ref={formRef}
100 | 					onSubmit={handleSubmit}
101 | 					className="flex-1 overflow-y-auto p-4 space-y-4"
102 | 				>
103 | 					{/* Title */}
104 | 					<div className="space-y-2">
105 | 						<Label htmlFor="title">Title</Label>
106 | 						<input
107 | 							ref={titleInputRef}
108 | 							id="title"
109 | 							type="text"
110 | 							value={updates.title || ''}
111 | 							onChange={(e) =>
112 | 								setUpdates({ ...updates, title: e.target.value })
113 | 							}
114 | 							className="w-full px-3 py-2 bg-vscode-input border border-vscode-border rounded-md text-vscode-foreground focus:outline-none focus:ring-2 focus:ring-vscode-focusBorder"
115 | 							placeholder="Task title"
116 | 						/>
117 | 					</div>
118 | 
119 | 					{/* Priority */}
120 | 					<div className="space-y-2">
121 | 						<Label htmlFor="priority">Priority</Label>
122 | 						<DropdownMenu>
123 | 							<DropdownMenuTrigger asChild>
124 | 								<Button variant="outline" className="w-full justify-between">
125 | 									<span className="capitalize">{updates.priority}</span>
126 | 									<svg
127 | 										className="w-4 h-4"
128 | 										fill="none"
129 | 										stroke="currentColor"
130 | 										viewBox="0 0 24 24"
131 | 									>
132 | 										<path
133 | 											strokeLinecap="round"
134 | 											strokeLinejoin="round"
135 | 											strokeWidth={2}
136 | 											d="M19 9l-7 7-7-7"
137 | 										/>
138 | 									</svg>
139 | 								</Button>
140 | 							</DropdownMenuTrigger>
141 | 							<DropdownMenuContent className="w-full">
142 | 								<DropdownMenuItem
143 | 									onClick={() => setUpdates({ ...updates, priority: 'high' })}
144 | 								>
145 | 									High
146 | 								</DropdownMenuItem>
147 | 								<DropdownMenuItem
148 | 									onClick={() => setUpdates({ ...updates, priority: 'medium' })}
149 | 								>
150 | 									Medium
151 | 								</DropdownMenuItem>
152 | 								<DropdownMenuItem
153 | 									onClick={() => setUpdates({ ...updates, priority: 'low' })}
154 | 								>
155 | 									Low
156 | 								</DropdownMenuItem>
157 | 							</DropdownMenuContent>
158 | 						</DropdownMenu>
159 | 					</div>
160 | 
161 | 					{/* Description */}
162 | 					<div className="space-y-2">
163 | 						<Label htmlFor="description">Description</Label>
164 | 						<Textarea
165 | 							id="description"
166 | 							value={updates.description || ''}
167 | 							onChange={(e) =>
168 | 								setUpdates({ ...updates, description: e.target.value })
169 | 							}
170 | 							className="min-h-[80px]"
171 | 							placeholder="Brief description of the task"
172 | 						/>
173 | 					</div>
174 | 
175 | 					{/* Details */}
176 | 					<div className="space-y-2">
177 | 						<Label htmlFor="details">Implementation Details</Label>
178 | 						<Textarea
179 | 							id="details"
180 | 							value={updates.details || ''}
181 | 							onChange={(e) =>
182 | 								setUpdates({ ...updates, details: e.target.value })
183 | 							}
184 | 							className="min-h-[120px]"
185 | 							placeholder="Technical details and implementation notes"
186 | 						/>
187 | 					</div>
188 | 
189 | 					{/* Test Strategy */}
190 | 					<div className="space-y-2">
191 | 						<Label htmlFor="testStrategy">Test Strategy</Label>
192 | 						<Textarea
193 | 							id="testStrategy"
194 | 							value={updates.testStrategy || ''}
195 | 							onChange={(e) =>
196 | 								setUpdates({ ...updates, testStrategy: e.target.value })
197 | 							}
198 | 							className="min-h-[80px]"
199 | 							placeholder="How to test this task"
200 | 						/>
201 | 					</div>
202 | 
203 | 					{/* Dependencies */}
204 | 					<div className="space-y-2">
205 | 						<Label htmlFor="dependencies">
206 | 							Dependencies (comma-separated task IDs)
207 | 						</Label>
208 | 						<input
209 | 							id="dependencies"
210 | 							type="text"
211 | 							value={updates.dependencies?.join(', ') || ''}
212 | 							onChange={(e) =>
213 | 								setUpdates({
214 | 									...updates,
215 | 									dependencies: e.target.value
216 | 										.split(',')
217 | 										.map((d) => d.trim())
218 | 										.filter(Boolean)
219 | 								})
220 | 							}
221 | 							className="w-full px-3 py-2 bg-vscode-input border border-vscode-border rounded-md text-vscode-foreground focus:outline-none focus:ring-2 focus:ring-vscode-focusBorder"
222 | 							placeholder="e.g., 1, 2.1, 3"
223 | 						/>
224 | 					</div>
225 | 				</form>
226 | 
227 | 				{/* Footer */}
228 | 				<div className="flex items-center justify-end gap-2 p-4 border-t border-vscode-border">
229 | 					<Button variant="outline" onClick={onCancel} disabled={isSaving}>
230 | 						Cancel
231 | 					</Button>
232 | 					<Button
233 | 						onClick={() => handleSubmit()}
234 | 						disabled={isSaving || !hasChanges()}
235 | 					>
236 | 						{isSaving ? 'Saving...' : 'Save Changes'}
237 | 					</Button>
238 | 				</div>
239 | 			</div>
240 | 		</div>
241 | 	);
242 | };
243 | 
```

--------------------------------------------------------------------------------
/tests/unit/ui.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * UI module tests
  3 |  */
  4 | 
  5 | import { jest } from '@jest/globals';
  6 | import {
  7 | 	getStatusWithColor,
  8 | 	formatDependenciesWithStatus,
  9 | 	createProgressBar,
 10 | 	getComplexityWithColor
 11 | } from '../../scripts/modules/ui.js';
 12 | import { sampleTasks } from '../fixtures/sample-tasks.js';
 13 | 
 14 | // Mock dependencies
 15 | jest.mock('chalk', () => {
 16 | 	const origChalkFn = (text) => text;
 17 | 	const chalk = origChalkFn;
 18 | 	chalk.green = (text) => text; // Return text as-is for status functions
 19 | 	chalk.yellow = (text) => text;
 20 | 	chalk.red = (text) => text;
 21 | 	chalk.cyan = (text) => text;
 22 | 	chalk.blue = (text) => text;
 23 | 	chalk.gray = (text) => text;
 24 | 	chalk.white = (text) => text;
 25 | 	chalk.bold = (text) => text;
 26 | 	chalk.dim = (text) => text;
 27 | 
 28 | 	// Add hex and other methods
 29 | 	chalk.hex = () => origChalkFn;
 30 | 	chalk.rgb = () => origChalkFn;
 31 | 
 32 | 	return chalk;
 33 | });
 34 | 
 35 | jest.mock('figlet', () => ({
 36 | 	textSync: jest.fn(() => 'Task Master Banner')
 37 | }));
 38 | 
 39 | jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`));
 40 | 
 41 | jest.mock('ora', () =>
 42 | 	jest.fn(() => ({
 43 | 		start: jest.fn(),
 44 | 		succeed: jest.fn(),
 45 | 		fail: jest.fn(),
 46 | 		stop: jest.fn()
 47 | 	}))
 48 | );
 49 | 
 50 | jest.mock('cli-table3', () =>
 51 | 	jest.fn().mockImplementation(() => ({
 52 | 		push: jest.fn(),
 53 | 		toString: jest.fn(() => 'Table Content')
 54 | 	}))
 55 | );
 56 | 
 57 | jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text)));
 58 | 
 59 | jest.mock('../../scripts/modules/utils.js', () => ({
 60 | 	CONFIG: {
 61 | 		projectName: 'Test Project',
 62 | 		projectVersion: '1.0.0'
 63 | 	},
 64 | 	log: jest.fn(),
 65 | 	findTaskById: jest.fn(),
 66 | 	readJSON: jest.fn(),
 67 | 	readComplexityReport: jest.fn(),
 68 | 	truncate: jest.fn((text) => text)
 69 | }));
 70 | 
 71 | jest.mock('../../scripts/modules/task-manager.js', () => ({
 72 | 	findNextTask: jest.fn(),
 73 | 	analyzeTaskComplexity: jest.fn()
 74 | }));
 75 | 
 76 | describe('UI Module', () => {
 77 | 	beforeEach(() => {
 78 | 		jest.clearAllMocks();
 79 | 	});
 80 | 
 81 | 	describe('getStatusWithColor function', () => {
 82 | 		test('should return done status with emoji for console output', () => {
 83 | 			const result = getStatusWithColor('done');
 84 | 			expect(result).toMatch(/done/);
 85 | 			expect(result).toContain('✓');
 86 | 		});
 87 | 
 88 | 		test('should return pending status with emoji for console output', () => {
 89 | 			const result = getStatusWithColor('pending');
 90 | 			expect(result).toMatch(/pending/);
 91 | 			expect(result).toContain('○');
 92 | 		});
 93 | 
 94 | 		test('should return deferred status with emoji for console output', () => {
 95 | 			const result = getStatusWithColor('deferred');
 96 | 			expect(result).toMatch(/deferred/);
 97 | 			expect(result).toContain('x');
 98 | 		});
 99 | 
100 | 		test('should return in-progress status with emoji for console output', () => {
101 | 			const result = getStatusWithColor('in-progress');
102 | 			expect(result).toMatch(/in-progress/);
103 | 			expect(result).toContain('🔄');
104 | 		});
105 | 
106 | 		test('should return unknown status with emoji for console output', () => {
107 | 			const result = getStatusWithColor('unknown');
108 | 			expect(result).toMatch(/unknown/);
109 | 			expect(result).toContain('❌');
110 | 		});
111 | 
112 | 		test('should use simple icons when forTable is true', () => {
113 | 			const doneResult = getStatusWithColor('done', true);
114 | 			expect(doneResult).toMatch(/done/);
115 | 			expect(doneResult).toContain('✓');
116 | 
117 | 			const pendingResult = getStatusWithColor('pending', true);
118 | 			expect(pendingResult).toMatch(/pending/);
119 | 			expect(pendingResult).toContain('○');
120 | 
121 | 			const inProgressResult = getStatusWithColor('in-progress', true);
122 | 			expect(inProgressResult).toMatch(/in-progress/);
123 | 			expect(inProgressResult).toContain('►');
124 | 
125 | 			const deferredResult = getStatusWithColor('deferred', true);
126 | 			expect(deferredResult).toMatch(/deferred/);
127 | 			expect(deferredResult).toContain('x');
128 | 		});
129 | 	});
130 | 
131 | 	describe('formatDependenciesWithStatus function', () => {
132 | 		test('should format dependencies as plain IDs when forConsole is false (default)', () => {
133 | 			const dependencies = [1, 2, 3];
134 | 			const allTasks = [
135 | 				{ id: 1, status: 'done' },
136 | 				{ id: 2, status: 'pending' },
137 | 				{ id: 3, status: 'deferred' }
138 | 			];
139 | 
140 | 			const result = formatDependenciesWithStatus(dependencies, allTasks);
141 | 
142 | 			// With recent changes, we expect just plain IDs when forConsole is false
143 | 			expect(result).toBe('1, 2, 3');
144 | 		});
145 | 
146 | 		test('should format dependencies with status indicators when forConsole is true', () => {
147 | 			const dependencies = [1, 2, 3];
148 | 			const allTasks = [
149 | 				{ id: 1, status: 'done' },
150 | 				{ id: 2, status: 'pending' },
151 | 				{ id: 3, status: 'deferred' }
152 | 			];
153 | 
154 | 			const result = formatDependenciesWithStatus(dependencies, allTasks, true);
155 | 
156 | 			// We can't test for exact color formatting due to our chalk mocks
157 | 			// Instead, test that the result contains all the expected IDs
158 | 			expect(result).toContain('1');
159 | 			expect(result).toContain('2');
160 | 			expect(result).toContain('3');
161 | 
162 | 			// Test that it's a comma-separated list
163 | 			expect(result.split(', ').length).toBe(3);
164 | 		});
165 | 
166 | 		test('should return "None" for empty dependencies', () => {
167 | 			const result = formatDependenciesWithStatus([], []);
168 | 			expect(result).toBe('None');
169 | 		});
170 | 
171 | 		test('should handle missing tasks in the task list', () => {
172 | 			const dependencies = [1, 999];
173 | 			const allTasks = [{ id: 1, status: 'done' }];
174 | 
175 | 			const result = formatDependenciesWithStatus(dependencies, allTasks);
176 | 			expect(result).toBe('1, 999 (Not found)');
177 | 		});
178 | 	});
179 | 
180 | 	describe('createProgressBar function', () => {
181 | 		test('should create a progress bar with the correct percentage', () => {
182 | 			const result = createProgressBar(50, 10, {
183 | 				pending: 20,
184 | 				'in-progress': 15,
185 | 				blocked: 5
186 | 			});
187 | 			expect(result).toContain('50%');
188 | 		});
189 | 
190 | 		test('should handle 0% progress', () => {
191 | 			const result = createProgressBar(0, 10);
192 | 			expect(result).toContain('0%');
193 | 		});
194 | 
195 | 		test('should handle 100% progress', () => {
196 | 			const result = createProgressBar(100, 10);
197 | 			expect(result).toContain('100%');
198 | 		});
199 | 
200 | 		test('should handle invalid percentages by clamping', () => {
201 | 			const result1 = createProgressBar(0, 10);
202 | 			expect(result1).toContain('0%');
203 | 
204 | 			const result2 = createProgressBar(100, 10);
205 | 			expect(result2).toContain('100%');
206 | 		});
207 | 
208 | 		test('should support status breakdown in the progress bar', () => {
209 | 			const result = createProgressBar(30, 10, {
210 | 				pending: 30,
211 | 				'in-progress': 20,
212 | 				blocked: 10,
213 | 				deferred: 5,
214 | 				cancelled: 5
215 | 			});
216 | 
217 | 			expect(result).toContain('40%');
218 | 		});
219 | 	});
220 | 
221 | 	describe('getComplexityWithColor function', () => {
222 | 		test('should return high complexity in red', () => {
223 | 			const result = getComplexityWithColor(8);
224 | 			expect(result).toMatch(/8/);
225 | 			expect(result).toContain('●');
226 | 		});
227 | 
228 | 		test('should return medium complexity in yellow', () => {
229 | 			const result = getComplexityWithColor(5);
230 | 			expect(result).toMatch(/5/);
231 | 			expect(result).toContain('●');
232 | 		});
233 | 
234 | 		test('should return low complexity in green', () => {
235 | 			const result = getComplexityWithColor(3);
236 | 			expect(result).toMatch(/3/);
237 | 			expect(result).toContain('●');
238 | 		});
239 | 
240 | 		test('should handle non-numeric inputs', () => {
241 | 			const result = getComplexityWithColor('high');
242 | 			expect(result).toMatch(/high/);
243 | 			expect(result).toContain('●');
244 | 		});
245 | 	});
246 | });
247 | 
```

--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/research.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * research.js
  3 |  * Direct function implementation for AI-powered research queries
  4 |  */
  5 | 
  6 | import path from 'path';
  7 | import { performResearch } from '../../../../scripts/modules/task-manager.js';
  8 | import {
  9 | 	enableSilentMode,
 10 | 	disableSilentMode
 11 | } from '../../../../scripts/modules/utils.js';
 12 | import { createLogWrapper } from '../../tools/utils.js';
 13 | 
 14 | /**
 15 |  * Direct function wrapper for performing AI-powered research with project context.
 16 |  *
 17 |  * @param {Object} args - Command arguments
 18 |  * @param {string} args.query - Research query/prompt (required)
 19 |  * @param {string} [args.taskIds] - Comma-separated list of task/subtask IDs for context
 20 |  * @param {string} [args.filePaths] - Comma-separated list of file paths for context
 21 |  * @param {string} [args.customContext] - Additional custom context text
 22 |  * @param {boolean} [args.includeProjectTree=false] - Include project file tree in context
 23 |  * @param {string} [args.detailLevel='medium'] - Detail level: 'low', 'medium', 'high'
 24 |  * @param {string} [args.saveTo] - Automatically save to task/subtask ID (e.g., "15" or "15.2")
 25 |  * @param {boolean} [args.saveToFile=false] - Save research results to .taskmaster/docs/research/ directory
 26 |  * @param {string} [args.projectRoot] - Project root path
 27 |  * @param {string} [args.tag] - Tag for the task (optional)
 28 |  * @param {Object} log - Logger object
 29 |  * @param {Object} context - Additional context (session)
 30 |  * @returns {Promise<Object>} - Result object { success: boolean, data?: any, error?: { code: string, message: string } }
 31 |  */
 32 | export async function researchDirect(args, log, context = {}) {
 33 | 	// Destructure expected args
 34 | 	const {
 35 | 		query,
 36 | 		taskIds,
 37 | 		filePaths,
 38 | 		customContext,
 39 | 		includeProjectTree = false,
 40 | 		detailLevel = 'medium',
 41 | 		saveTo,
 42 | 		saveToFile = false,
 43 | 		projectRoot,
 44 | 		tag
 45 | 	} = args;
 46 | 	const { session } = context; // Destructure session from context
 47 | 
 48 | 	// Enable silent mode to prevent console logs from interfering with JSON response
 49 | 	enableSilentMode();
 50 | 
 51 | 	// Create logger wrapper using the utility
 52 | 	const mcpLog = createLogWrapper(log);
 53 | 
 54 | 	try {
 55 | 		// Check required parameters
 56 | 		if (!query || typeof query !== 'string' || query.trim().length === 0) {
 57 | 			log.error('Missing or invalid required parameter: query');
 58 | 			disableSilentMode();
 59 | 			return {
 60 | 				success: false,
 61 | 				error: {
 62 | 					code: 'MISSING_PARAMETER',
 63 | 					message:
 64 | 						'The query parameter is required and must be a non-empty string'
 65 | 				}
 66 | 			};
 67 | 		}
 68 | 
 69 | 		// Parse comma-separated task IDs if provided
 70 | 		const parsedTaskIds = taskIds
 71 | 			? taskIds
 72 | 					.split(',')
 73 | 					.map((id) => id.trim())
 74 | 					.filter((id) => id.length > 0)
 75 | 			: [];
 76 | 
 77 | 		// Parse comma-separated file paths if provided
 78 | 		const parsedFilePaths = filePaths
 79 | 			? filePaths
 80 | 					.split(',')
 81 | 					.map((path) => path.trim())
 82 | 					.filter((path) => path.length > 0)
 83 | 			: [];
 84 | 
 85 | 		// Validate detail level
 86 | 		const validDetailLevels = ['low', 'medium', 'high'];
 87 | 		if (!validDetailLevels.includes(detailLevel)) {
 88 | 			log.error(`Invalid detail level: ${detailLevel}`);
 89 | 			disableSilentMode();
 90 | 			return {
 91 | 				success: false,
 92 | 				error: {
 93 | 					code: 'INVALID_PARAMETER',
 94 | 					message: `Detail level must be one of: ${validDetailLevels.join(', ')}`
 95 | 				}
 96 | 			};
 97 | 		}
 98 | 
 99 | 		log.info(
100 | 			`Performing research query: "${query.substring(0, 100)}${query.length > 100 ? '...' : ''}", ` +
101 | 				`taskIds: [${parsedTaskIds.join(', ')}], ` +
102 | 				`filePaths: [${parsedFilePaths.join(', ')}], ` +
103 | 				`detailLevel: ${detailLevel}, ` +
104 | 				`includeProjectTree: ${includeProjectTree}, ` +
105 | 				`projectRoot: ${projectRoot}`
106 | 		);
107 | 
108 | 		// Prepare options for the research function
109 | 		const researchOptions = {
110 | 			taskIds: parsedTaskIds,
111 | 			filePaths: parsedFilePaths,
112 | 			customContext: customContext || '',
113 | 			includeProjectTree,
114 | 			detailLevel,
115 | 			projectRoot,
116 | 			tag,
117 | 			saveToFile
118 | 		};
119 | 
120 | 		// Prepare context for the research function
121 | 		const researchContext = {
122 | 			session,
123 | 			mcpLog,
124 | 			commandName: 'research',
125 | 			outputType: 'mcp'
126 | 		};
127 | 
128 | 		// Call the performResearch function
129 | 		const result = await performResearch(
130 | 			query.trim(),
131 | 			researchOptions,
132 | 			researchContext,
133 | 			'json', // outputFormat - use 'json' to suppress CLI UI
134 | 			false // allowFollowUp - disable for MCP calls
135 | 		);
136 | 
137 | 		// Auto-save to task/subtask if requested
138 | 		if (saveTo) {
139 | 			try {
140 | 				const isSubtask = saveTo.includes('.');
141 | 
142 | 				// Format research content for saving
143 | 				const researchContent = `## Research Query: ${query.trim()}
144 | 
145 | **Detail Level:** ${result.detailLevel}
146 | **Context Size:** ${result.contextSize} characters
147 | **Timestamp:** ${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}
148 | 
149 | ### Results
150 | 
151 | ${result.result}`;
152 | 
153 | 				if (isSubtask) {
154 | 					// Save to subtask
155 | 					const { updateSubtaskById } = await import(
156 | 						'../../../../scripts/modules/task-manager/update-subtask-by-id.js'
157 | 					);
158 | 
159 | 					const tasksPath = path.join(
160 | 						projectRoot,
161 | 						'.taskmaster',
162 | 						'tasks',
163 | 						'tasks.json'
164 | 					);
165 | 					await updateSubtaskById(
166 | 						tasksPath,
167 | 						saveTo,
168 | 						researchContent,
169 | 						false, // useResearch = false for simple append
170 | 						{
171 | 							session,
172 | 							mcpLog,
173 | 							commandName: 'research-save',
174 | 							outputType: 'mcp',
175 | 							projectRoot,
176 | 							tag
177 | 						},
178 | 						'json'
179 | 					);
180 | 
181 | 					log.info(`Research saved to subtask ${saveTo}`);
182 | 				} else {
183 | 					// Save to task
184 | 					const updateTaskById = (
185 | 						await import(
186 | 							'../../../../scripts/modules/task-manager/update-task-by-id.js'
187 | 						)
188 | 					).default;
189 | 
190 | 					const taskIdNum = parseInt(saveTo, 10);
191 | 					const tasksPath = path.join(
192 | 						projectRoot,
193 | 						'.taskmaster',
194 | 						'tasks',
195 | 						'tasks.json'
196 | 					);
197 | 					await updateTaskById(
198 | 						tasksPath,
199 | 						taskIdNum,
200 | 						researchContent,
201 | 						false, // useResearch = false for simple append
202 | 						{
203 | 							session,
204 | 							mcpLog,
205 | 							commandName: 'research-save',
206 | 							outputType: 'mcp',
207 | 							projectRoot,
208 | 							tag
209 | 						},
210 | 						'json',
211 | 						true // appendMode = true
212 | 					);
213 | 
214 | 					log.info(`Research saved to task ${saveTo}`);
215 | 				}
216 | 			} catch (saveError) {
217 | 				log.warn(`Error saving research to task/subtask: ${saveError.message}`);
218 | 			}
219 | 		}
220 | 
221 | 		// Restore normal logging
222 | 		disableSilentMode();
223 | 
224 | 		return {
225 | 			success: true,
226 | 			data: {
227 | 				query: result.query,
228 | 				result: result.result,
229 | 				contextSize: result.contextSize,
230 | 				contextTokens: result.contextTokens,
231 | 				tokenBreakdown: result.tokenBreakdown,
232 | 				systemPromptTokens: result.systemPromptTokens,
233 | 				userPromptTokens: result.userPromptTokens,
234 | 				totalInputTokens: result.totalInputTokens,
235 | 				detailLevel: result.detailLevel,
236 | 				telemetryData: result.telemetryData,
237 | 				tagInfo: result.tagInfo,
238 | 				savedFilePath: result.savedFilePath
239 | 			}
240 | 		};
241 | 	} catch (error) {
242 | 		// Make sure to restore normal logging even if there's an error
243 | 		disableSilentMode();
244 | 
245 | 		log.error(`Error in researchDirect: ${error.message}`);
246 | 		return {
247 | 			success: false,
248 | 			error: {
249 | 				code: error.code || 'RESEARCH_ERROR',
250 | 				message: error.message
251 | 			}
252 | 		};
253 | 	}
254 | }
255 | 
```

--------------------------------------------------------------------------------
/mcp-server/src/core/direct-functions/analyze-task-complexity.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Direct function wrapper for analyzeTaskComplexity
  3 |  */
  4 | 
  5 | import analyzeTaskComplexity from '../../../../scripts/modules/task-manager/analyze-task-complexity.js';
  6 | import {
  7 | 	enableSilentMode,
  8 | 	disableSilentMode,
  9 | 	isSilentMode
 10 | } from '../../../../scripts/modules/utils.js';
 11 | import fs from 'fs';
 12 | import { createLogWrapper } from '../../tools/utils.js'; // Import the new utility
 13 | 
 14 | /**
 15 |  * Analyze task complexity and generate recommendations
 16 |  * @param {Object} args - Function arguments
 17 |  * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file.
 18 |  * @param {string} args.outputPath - Explicit absolute path to save the report.
 19 |  * @param {string|number} [args.threshold] - Minimum complexity score to recommend expansion (1-10)
 20 |  * @param {boolean} [args.research] - Use Perplexity AI for research-backed complexity analysis
 21 |  * @param {string} [args.ids] - Comma-separated list of task IDs to analyze
 22 |  * @param {number} [args.from] - Starting task ID in a range to analyze
 23 |  * @param {number} [args.to] - Ending task ID in a range to analyze
 24 |  * @param {string} [args.projectRoot] - Project root path.
 25 |  * @param {string} [args.tag] - Tag for the task (optional)
 26 |  * @param {Object} log - Logger object
 27 |  * @param {Object} [context={}] - Context object containing session data
 28 |  * @param {Object} [context.session] - MCP session object
 29 |  * @returns {Promise<{success: boolean, data?: Object, error?: {code: string, message: string}}>}
 30 |  */
 31 | export async function analyzeTaskComplexityDirect(args, log, context = {}) {
 32 | 	const { session } = context;
 33 | 	const {
 34 | 		tasksJsonPath,
 35 | 		outputPath,
 36 | 		threshold,
 37 | 		research,
 38 | 		projectRoot,
 39 | 		ids,
 40 | 		from,
 41 | 		to,
 42 | 		tag
 43 | 	} = args;
 44 | 
 45 | 	const logWrapper = createLogWrapper(log);
 46 | 
 47 | 	// --- Initial Checks (remain the same) ---
 48 | 	try {
 49 | 		log.info(`Analyzing task complexity with args: ${JSON.stringify(args)}`);
 50 | 
 51 | 		if (!tasksJsonPath) {
 52 | 			log.error('analyzeTaskComplexityDirect called without tasksJsonPath');
 53 | 			return {
 54 | 				success: false,
 55 | 				error: {
 56 | 					code: 'MISSING_ARGUMENT',
 57 | 					message: 'tasksJsonPath is required'
 58 | 				}
 59 | 			};
 60 | 		}
 61 | 		if (!outputPath) {
 62 | 			log.error('analyzeTaskComplexityDirect called without outputPath');
 63 | 			return {
 64 | 				success: false,
 65 | 				error: { code: 'MISSING_ARGUMENT', message: 'outputPath is required' }
 66 | 			};
 67 | 		}
 68 | 
 69 | 		const tasksPath = tasksJsonPath;
 70 | 		const resolvedOutputPath = outputPath;
 71 | 
 72 | 		log.info(`Analyzing task complexity from: ${tasksPath}`);
 73 | 		log.info(`Output report will be saved to: ${resolvedOutputPath}`);
 74 | 
 75 | 		if (ids) {
 76 | 			log.info(`Analyzing specific task IDs: ${ids}`);
 77 | 		} else if (from || to) {
 78 | 			const fromStr = from !== undefined ? from : 'first';
 79 | 			const toStr = to !== undefined ? to : 'last';
 80 | 			log.info(`Analyzing tasks in range: ${fromStr} to ${toStr}`);
 81 | 		}
 82 | 
 83 | 		if (research) {
 84 | 			log.info('Using research role for complexity analysis');
 85 | 		}
 86 | 
 87 | 		// Prepare options for the core function - REMOVED mcpLog and session here
 88 | 		const coreOptions = {
 89 | 			file: tasksJsonPath,
 90 | 			output: outputPath,
 91 | 			threshold: threshold,
 92 | 			research: research === true, // Ensure boolean
 93 | 			projectRoot: projectRoot, // Pass projectRoot here
 94 | 			id: ids, // Pass the ids parameter to the core function as 'id'
 95 | 			from: from, // Pass from parameter
 96 | 			to: to, // Pass to parameter
 97 | 			tag // forward tag
 98 | 		};
 99 | 		// --- End Initial Checks ---
100 | 
101 | 		// --- Silent Mode and Logger Wrapper ---
102 | 		const wasSilent = isSilentMode();
103 | 		if (!wasSilent) {
104 | 			enableSilentMode(); // Still enable silent mode as a backup
105 | 		}
106 | 
107 | 		let report;
108 | 		let coreResult;
109 | 
110 | 		try {
111 | 			// --- Call Core Function (Pass context separately) ---
112 | 			// Pass coreOptions as the first argument
113 | 			// Pass context object { session, mcpLog } as the second argument
114 | 			coreResult = await analyzeTaskComplexity(coreOptions, {
115 | 				session,
116 | 				mcpLog: logWrapper,
117 | 				commandName: 'analyze-complexity',
118 | 				outputType: 'mcp',
119 | 				projectRoot,
120 | 				tag
121 | 			});
122 | 			report = coreResult.report;
123 | 		} catch (error) {
124 | 			log.error(
125 | 				`Error in analyzeTaskComplexity core function: ${error.message}`
126 | 			);
127 | 			// Restore logging if we changed it
128 | 			if (!wasSilent && isSilentMode()) {
129 | 				disableSilentMode();
130 | 			}
131 | 			return {
132 | 				success: false,
133 | 				error: {
134 | 					code: 'ANALYZE_CORE_ERROR',
135 | 					message: `Error running core complexity analysis: ${error.message}`
136 | 				}
137 | 			};
138 | 		} finally {
139 | 			// Always restore normal logging in finally block if we enabled silent mode
140 | 			if (!wasSilent && isSilentMode()) {
141 | 				disableSilentMode();
142 | 			}
143 | 		}
144 | 
145 | 		// --- Result Handling (remains largely the same) ---
146 | 		// Verify the report file was created (core function writes it)
147 | 		if (!fs.existsSync(resolvedOutputPath)) {
148 | 			return {
149 | 				success: false,
150 | 				error: {
151 | 					code: 'ANALYZE_REPORT_MISSING', // Specific code
152 | 					message:
153 | 						'Analysis completed but no report file was created at the expected path.'
154 | 				}
155 | 			};
156 | 		}
157 | 
158 | 		if (
159 | 			!coreResult ||
160 | 			!coreResult.report ||
161 | 			typeof coreResult.report !== 'object'
162 | 		) {
163 | 			log.error(
164 | 				'Core analysis function returned an invalid or undefined response.'
165 | 			);
166 | 			return {
167 | 				success: false,
168 | 				error: {
169 | 					code: 'INVALID_CORE_RESPONSE',
170 | 					message: 'Core analysis function returned an invalid response.'
171 | 				}
172 | 			};
173 | 		}
174 | 
175 | 		try {
176 | 			// Ensure complexityAnalysis exists and is an array
177 | 			const analysisArray = Array.isArray(coreResult.report.complexityAnalysis)
178 | 				? coreResult.report.complexityAnalysis
179 | 				: [];
180 | 
181 | 			// Count tasks by complexity (remains the same)
182 | 			const highComplexityTasks = analysisArray.filter(
183 | 				(t) => t.complexityScore >= 8
184 | 			).length;
185 | 			const mediumComplexityTasks = analysisArray.filter(
186 | 				(t) => t.complexityScore >= 5 && t.complexityScore < 8
187 | 			).length;
188 | 			const lowComplexityTasks = analysisArray.filter(
189 | 				(t) => t.complexityScore < 5
190 | 			).length;
191 | 
192 | 			return {
193 | 				success: true,
194 | 				data: {
195 | 					message: `Task complexity analysis complete. Report saved to ${outputPath}`,
196 | 					reportPath: outputPath,
197 | 					reportSummary: {
198 | 						taskCount: analysisArray.length,
199 | 						highComplexityTasks,
200 | 						mediumComplexityTasks,
201 | 						lowComplexityTasks
202 | 					},
203 | 					fullReport: coreResult.report,
204 | 					telemetryData: coreResult.telemetryData,
205 | 					tagInfo: coreResult.tagInfo
206 | 				}
207 | 			};
208 | 		} catch (parseError) {
209 | 			// Should not happen if core function returns object, but good safety check
210 | 			log.error(`Internal error processing report data: ${parseError.message}`);
211 | 			return {
212 | 				success: false,
213 | 				error: {
214 | 					code: 'REPORT_PROCESS_ERROR',
215 | 					message: `Internal error processing complexity report: ${parseError.message}`
216 | 				}
217 | 			};
218 | 		}
219 | 		// --- End Result Handling ---
220 | 	} catch (error) {
221 | 		// Catch errors from initial checks or path resolution
222 | 		// Make sure to restore normal logging if silent mode was enabled
223 | 		if (isSilentMode()) {
224 | 			disableSilentMode();
225 | 		}
226 | 		log.error(`Error in analyzeTaskComplexityDirect setup: ${error.message}`);
227 | 		return {
228 | 			success: false,
229 | 			error: {
230 | 				code: 'DIRECT_FUNCTION_SETUP_ERROR',
231 | 				message: error.message
232 | 			}
233 | 		};
234 | 	}
235 | }
236 | 
```

--------------------------------------------------------------------------------
/tests/unit/ai-providers/claude-code.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | 
  3 | // Mock the ai-sdk-provider-claude-code package
  4 | jest.unstable_mockModule('ai-sdk-provider-claude-code', () => ({
  5 | 	createClaudeCode: jest.fn(() => {
  6 | 		const provider = (modelId, settings) => ({
  7 | 			// Minimal mock language model surface
  8 | 			id: modelId,
  9 | 			settings,
 10 | 			doGenerate: jest.fn(() => ({ text: 'ok', usage: {} })),
 11 | 			doStream: jest.fn(() => ({ stream: true }))
 12 | 		});
 13 | 		provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
 14 | 		provider.chat = provider.languageModel;
 15 | 		return provider;
 16 | 	})
 17 | }));
 18 | 
 19 | // Mock the base provider
 20 | jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
 21 | 	BaseAIProvider: class {
 22 | 		constructor() {
 23 | 			this.name = 'Base Provider';
 24 | 		}
 25 | 		handleError(context, error) {
 26 | 			throw error;
 27 | 		}
 28 | 	}
 29 | }));
 30 | 
 31 | // Mock config getters
 32 | jest.unstable_mockModule('../../../scripts/modules/config-manager.js', () => ({
 33 | 	getClaudeCodeSettingsForCommand: jest.fn(() => ({})),
 34 | 	getSupportedModelsForProvider: jest.fn(() => ['opus', 'sonnet', 'haiku']),
 35 | 	getDebugFlag: jest.fn(() => false),
 36 | 	getLogLevel: jest.fn(() => 'info')
 37 | }));
 38 | 
 39 | // Import after mocking
 40 | const { ClaudeCodeProvider } = await import(
 41 | 	'../../../src/ai-providers/claude-code.js'
 42 | );
 43 | 
 44 | describe('ClaudeCodeProvider', () => {
 45 | 	let provider;
 46 | 
 47 | 	beforeEach(() => {
 48 | 		provider = new ClaudeCodeProvider();
 49 | 		jest.clearAllMocks();
 50 | 	});
 51 | 
 52 | 	describe('constructor', () => {
 53 | 		it('should set the provider name to Claude Code', () => {
 54 | 			expect(provider.name).toBe('Claude Code');
 55 | 		});
 56 | 	});
 57 | 
 58 | 	describe('validateAuth', () => {
 59 | 		it('should not throw an error (no API key required)', () => {
 60 | 			expect(() => provider.validateAuth({})).not.toThrow();
 61 | 		});
 62 | 
 63 | 		it('should not require any parameters', () => {
 64 | 			expect(() => provider.validateAuth()).not.toThrow();
 65 | 		});
 66 | 
 67 | 		it('should work with any params passed', () => {
 68 | 			expect(() =>
 69 | 				provider.validateAuth({
 70 | 					apiKey: 'some-key',
 71 | 					baseURL: 'https://example.com'
 72 | 				})
 73 | 			).not.toThrow();
 74 | 		});
 75 | 	});
 76 | 
 77 | 	describe('getClient', () => {
 78 | 		it('should return a claude code client', () => {
 79 | 			const client = provider.getClient({});
 80 | 			expect(client).toBeDefined();
 81 | 			expect(typeof client).toBe('function');
 82 | 		});
 83 | 
 84 | 		it('should create client without parameters', () => {
 85 | 			const client = provider.getClient();
 86 | 			expect(client).toBeDefined();
 87 | 		});
 88 | 
 89 | 		it('should handle commandName parameter', () => {
 90 | 			const client = provider.getClient({
 91 | 				commandName: 'test-command'
 92 | 			});
 93 | 			expect(client).toBeDefined();
 94 | 		});
 95 | 
 96 | 		it('should have languageModel and chat methods', () => {
 97 | 			const client = provider.getClient({});
 98 | 			expect(client.languageModel).toBeDefined();
 99 | 			expect(client.chat).toBeDefined();
100 | 			expect(client.chat).toBe(client.languageModel);
101 | 		});
102 | 
103 | 		it('should pass systemPrompt configuration to createClaudeCode', async () => {
104 | 			const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
105 | 
106 | 			provider.getClient({});
107 | 
108 | 			expect(createClaudeCode).toHaveBeenCalledWith(
109 | 				expect.objectContaining({
110 | 					defaultSettings: expect.objectContaining({
111 | 						systemPrompt: {
112 | 							type: 'preset',
113 | 							preset: 'claude_code'
114 | 						}
115 | 					})
116 | 				})
117 | 			);
118 | 		});
119 | 
120 | 		it('should pass settingSources configuration to createClaudeCode', async () => {
121 | 			const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
122 | 
123 | 			provider.getClient({});
124 | 
125 | 			expect(createClaudeCode).toHaveBeenCalledWith(
126 | 				expect.objectContaining({
127 | 					defaultSettings: expect.objectContaining({
128 | 						settingSources: ['user', 'project', 'local']
129 | 					})
130 | 				})
131 | 			);
132 | 		});
133 | 
134 | 		it('should pass defaultSettings from config to createClaudeCode', async () => {
135 | 			const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
136 | 			const { getClaudeCodeSettingsForCommand } = await import(
137 | 				'../../../scripts/modules/config-manager.js'
138 | 			);
139 | 
140 | 			const mockSettings = { maxTokens: 4096, temperature: 0.7 };
141 | 			getClaudeCodeSettingsForCommand.mockReturnValueOnce(mockSettings);
142 | 
143 | 			provider.getClient({ commandName: 'test-command' });
144 | 
145 | 			expect(createClaudeCode).toHaveBeenCalledWith(
146 | 				expect.objectContaining({
147 | 					defaultSettings: expect.objectContaining({
148 | 						...mockSettings,
149 | 						systemPrompt: {
150 | 							type: 'preset',
151 | 							preset: 'claude_code'
152 | 						},
153 | 						settingSources: ['user', 'project', 'local']
154 | 					})
155 | 				})
156 | 			);
157 | 		});
158 | 
159 | 		it('should pass complete configuration object to createClaudeCode', async () => {
160 | 			const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
161 | 			const { getClaudeCodeSettingsForCommand } = await import(
162 | 				'../../../scripts/modules/config-manager.js'
163 | 			);
164 | 
165 | 			const mockSettings = { maxTokens: 2048 };
166 | 			getClaudeCodeSettingsForCommand.mockReturnValueOnce(mockSettings);
167 | 
168 | 			provider.getClient({ commandName: 'analyze' });
169 | 
170 | 			// Verify the complete configuration structure matches v2.0 migration requirements
171 | 			expect(createClaudeCode).toHaveBeenCalledWith({
172 | 				defaultSettings: {
173 | 					...mockSettings,
174 | 					// Restores pre-2.0 behavior: explicit system prompt preset
175 | 					systemPrompt: {
176 | 						type: 'preset',
177 | 						preset: 'claude_code'
178 | 					},
179 | 					// Restores pre-2.0 behavior: enables loading of CLAUDE.md and settings.json
180 | 					settingSources: ['user', 'project', 'local']
181 | 				}
182 | 			});
183 | 		});
184 | 
185 | 		it('should pass empty defaultSettings when config returns null', async () => {
186 | 			const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
187 | 			const { getClaudeCodeSettingsForCommand } = await import(
188 | 				'../../../scripts/modules/config-manager.js'
189 | 			);
190 | 
191 | 			getClaudeCodeSettingsForCommand.mockReturnValueOnce(null);
192 | 
193 | 			provider.getClient({});
194 | 
195 | 			expect(createClaudeCode).toHaveBeenCalledWith(
196 | 				expect.objectContaining({
197 | 					defaultSettings: expect.objectContaining({
198 | 						systemPrompt: {
199 | 							type: 'preset',
200 | 							preset: 'claude_code'
201 | 						},
202 | 						settingSources: ['user', 'project', 'local']
203 | 					})
204 | 				})
205 | 			);
206 | 		});
207 | 	});
208 | 
209 | 	describe('model support', () => {
210 | 		it('should return supported models', () => {
211 | 			const models = provider.getSupportedModels();
212 | 			expect(models).toEqual(['opus', 'sonnet', 'haiku']);
213 | 		});
214 | 
215 | 		it('should check if model is supported', () => {
216 | 			expect(provider.isModelSupported('sonnet')).toBe(true);
217 | 			expect(provider.isModelSupported('opus')).toBe(true);
218 | 			expect(provider.isModelSupported('haiku')).toBe(true);
219 | 			expect(provider.isModelSupported('unknown')).toBe(false);
220 | 		});
221 | 	});
222 | 
223 | 	describe('error handling', () => {
224 | 		it('should handle client initialization errors', async () => {
225 | 			// Force an error by making createClaudeCode throw
226 | 			const { createClaudeCode } = await import('ai-sdk-provider-claude-code');
227 | 			createClaudeCode.mockImplementationOnce(() => {
228 | 				throw new Error('Mock initialization error');
229 | 			});
230 | 
231 | 			// Create a new provider instance to use the mocked createClaudeCode
232 | 			const errorProvider = new ClaudeCodeProvider();
233 | 			expect(() => errorProvider.getClient({})).toThrow(
234 | 				'Mock initialization error'
235 | 			);
236 | 		});
237 | 	});
238 | });
239 | 
```

--------------------------------------------------------------------------------
/tests/unit/task-manager/tag-boundary.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import fs from 'fs';
  2 | import path from 'path';
  3 | import {
  4 | 	createTag,
  5 | 	useTag,
  6 | 	deleteTag
  7 | } from '../../../scripts/modules/task-manager/tag-management.js';
  8 | 
  9 | // Temporary workspace for each test run
 10 | const TEMP_DIR = path.join(process.cwd(), '.tmp_tag_boundary');
 11 | const TASKS_PATH = path.join(TEMP_DIR, 'tasks.json');
 12 | const STATE_PATH = path.join(TEMP_DIR, '.taskmaster', 'state.json');
 13 | 
 14 | function seedWorkspace() {
 15 | 	// Reset temp dir
 16 | 	fs.rmSync(TEMP_DIR, { recursive: true, force: true });
 17 | 	fs.mkdirSync(path.join(TEMP_DIR, '.taskmaster'), {
 18 | 		recursive: true,
 19 | 		force: true
 20 | 	});
 21 | 
 22 | 	// Minimal master tag file
 23 | 	fs.writeFileSync(
 24 | 		TASKS_PATH,
 25 | 		JSON.stringify(
 26 | 			{
 27 | 				master: {
 28 | 					tasks: [{ id: 1, title: 'Seed task', status: 'pending' }],
 29 | 					metadata: { created: new Date().toISOString() }
 30 | 				}
 31 | 			},
 32 | 			null,
 33 | 			2
 34 | 		),
 35 | 		'utf8'
 36 | 	);
 37 | 
 38 | 	// Initial state.json
 39 | 	fs.writeFileSync(
 40 | 		STATE_PATH,
 41 | 		JSON.stringify(
 42 | 			{ currentTag: 'master', lastSwitched: new Date().toISOString() },
 43 | 			null,
 44 | 			2
 45 | 		),
 46 | 		'utf8'
 47 | 	);
 48 | }
 49 | 
 50 | describe('Tag boundary resolution', () => {
 51 | 	beforeEach(seedWorkspace);
 52 | 	afterAll(() => fs.rmSync(TEMP_DIR, { recursive: true, force: true }));
 53 | 
 54 | 	it('switches currentTag in state.json when useTag succeeds', async () => {
 55 | 		await createTag(
 56 | 			TASKS_PATH,
 57 | 			'feature-x',
 58 | 			{},
 59 | 			{ projectRoot: TEMP_DIR },
 60 | 			'json'
 61 | 		);
 62 | 		await useTag(
 63 | 			TASKS_PATH,
 64 | 			'feature-x',
 65 | 			{},
 66 | 			{ projectRoot: TEMP_DIR },
 67 | 			'json'
 68 | 		);
 69 | 
 70 | 		const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
 71 | 		expect(state.currentTag).toBe('feature-x');
 72 | 	});
 73 | 
 74 | 	it('throws error when switching to non-existent tag', async () => {
 75 | 		await expect(
 76 | 			useTag(TASKS_PATH, 'ghost', {}, { projectRoot: TEMP_DIR }, 'json')
 77 | 		).rejects.toThrow(/does not exist/);
 78 | 	});
 79 | 
 80 | 	it('deleting active tag auto-switches back to master', async () => {
 81 | 		await createTag(TASKS_PATH, 'temp', {}, { projectRoot: TEMP_DIR }, 'json');
 82 | 		await useTag(TASKS_PATH, 'temp', {}, { projectRoot: TEMP_DIR }, 'json');
 83 | 
 84 | 		// Delete the active tag with force flag (yes: true)
 85 | 		await deleteTag(
 86 | 			TASKS_PATH,
 87 | 			'temp',
 88 | 			{ yes: true },
 89 | 			{ projectRoot: TEMP_DIR },
 90 | 			'json'
 91 | 		);
 92 | 
 93 | 		const state = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
 94 | 		expect(state.currentTag).toBe('master');
 95 | 
 96 | 		const tasksFile = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
 97 | 		expect(tasksFile.temp).toBeUndefined();
 98 | 		expect(tasksFile.master).toBeDefined();
 99 | 	});
100 | 
101 | 	it('createTag with copyFromCurrent deep-copies tasks (mutation isolated)', async () => {
102 | 		// create new tag with copy
103 | 		await createTag(
104 | 			TASKS_PATH,
105 | 			'alpha',
106 | 			{ copyFromCurrent: true },
107 | 			{ projectRoot: TEMP_DIR },
108 | 			'json'
109 | 		);
110 | 
111 | 		// mutate a field inside alpha tasks
112 | 		const updatedData = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
113 | 		updatedData.alpha.tasks[0].title = 'Changed in alpha';
114 | 		fs.writeFileSync(TASKS_PATH, JSON.stringify(updatedData, null, 2));
115 | 
116 | 		const finalData = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
117 | 		expect(finalData.master.tasks[0].title).toBe('Seed task');
118 | 		expect(finalData.alpha.tasks[0].title).toBe('Changed in alpha');
119 | 	});
120 | 
121 | 	it('addTask to non-master tag does not leak into master', async () => {
122 | 		// create and switch
123 | 		await createTag(
124 | 			TASKS_PATH,
125 | 			'feature-api',
126 | 			{},
127 | 			{ projectRoot: TEMP_DIR },
128 | 			'json'
129 | 		);
130 | 
131 | 		// Call addTask with manual data to avoid AI
132 | 		const { default: addTask } = await import(
133 | 			'../../../scripts/modules/task-manager/add-task.js'
134 | 		);
135 | 
136 | 		await addTask(
137 | 			TASKS_PATH,
138 | 			'Manual task',
139 | 			[],
140 | 			null,
141 | 			{ projectRoot: TEMP_DIR, tag: 'feature-api' },
142 | 			'json',
143 | 			{
144 | 				title: 'API work',
145 | 				description: 'Implement endpoint',
146 | 				details: 'Details',
147 | 				testStrategy: 'Tests'
148 | 			},
149 | 			false
150 | 		);
151 | 
152 | 		const data = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
153 | 		expect(data['feature-api'].tasks.length).toBe(1); // the new task only
154 | 		expect(data.master.tasks.length).toBe(1); // still only seed
155 | 	});
156 | 
157 | 	it('reserved tag names are rejected', async () => {
158 | 		await expect(
159 | 			createTag(TASKS_PATH, 'master', {}, { projectRoot: TEMP_DIR }, 'json')
160 | 		).rejects.toThrow(/reserved tag/i);
161 | 	});
162 | 
163 | 	it('cannot delete the master tag', async () => {
164 | 		await expect(
165 | 			deleteTag(
166 | 				TASKS_PATH,
167 | 				'master',
168 | 				{ yes: true },
169 | 				{ projectRoot: TEMP_DIR },
170 | 				'json'
171 | 			)
172 | 		).rejects.toThrow(/Cannot delete the "master" tag/);
173 | 	});
174 | 
175 | 	it('copyTag deep copy – mutation does not affect source', async () => {
176 | 		const { copyTag } = await import(
177 | 			'../../../scripts/modules/task-manager/tag-management.js'
178 | 		);
179 | 
180 | 		await createTag(
181 | 			TASKS_PATH,
182 | 			'source',
183 | 			{ copyFromCurrent: true },
184 | 			{ projectRoot: TEMP_DIR },
185 | 			'json'
186 | 		);
187 | 		await copyTag(
188 | 			TASKS_PATH,
189 | 			'source',
190 | 			'clone',
191 | 			{},
192 | 			{ projectRoot: TEMP_DIR },
193 | 			'json'
194 | 		);
195 | 
196 | 		// mutate clone task title
197 | 		const data1 = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
198 | 		data1.clone.tasks[0].title = 'Modified in clone';
199 | 		fs.writeFileSync(TASKS_PATH, JSON.stringify(data1, null, 2));
200 | 
201 | 		const data2 = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
202 | 		expect(data2.source.tasks[0].title).toBe('Seed task');
203 | 		expect(data2.clone.tasks[0].title).toBe('Modified in clone');
204 | 	});
205 | 
206 | 	it('adds task to tag derived from state.json when no explicit tag supplied', async () => {
207 | 		// Create new tag and update state.json to make it current
208 | 		await createTag(
209 | 			TASKS_PATH,
210 | 			'feature-auto',
211 | 			{},
212 | 			{ projectRoot: TEMP_DIR },
213 | 			'json'
214 | 		);
215 | 		const state1 = JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
216 | 		state1.currentTag = 'feature-auto';
217 | 		fs.writeFileSync(STATE_PATH, JSON.stringify(state1, null, 2));
218 | 
219 | 		const { default: addTask } = await import(
220 | 			'../../../scripts/modules/task-manager/add-task.js'
221 | 		);
222 | 		const { resolveTag } = await import('../../../scripts/modules/utils.js');
223 | 
224 | 		const tag = resolveTag({ projectRoot: TEMP_DIR });
225 | 
226 | 		// Add task without passing tag -> should resolve to feature-auto
227 | 		await addTask(
228 | 			TASKS_PATH,
229 | 			'Auto task',
230 | 			[],
231 | 			null,
232 | 			{ projectRoot: TEMP_DIR, tag },
233 | 			'json',
234 | 			{
235 | 				title: 'Auto task',
236 | 				description: '-',
237 | 				details: '-',
238 | 				testStrategy: '-'
239 | 			},
240 | 			false
241 | 		);
242 | 
243 | 		const data = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
244 | 		expect(data['feature-auto'].tasks.length).toBe(1);
245 | 		expect(data.master.tasks.length).toBe(1); // master unchanged
246 | 	});
247 | 
248 | 	it('falls back to master when state.json lacks currentTag', async () => {
249 | 		// wipe currentTag field
250 | 		fs.writeFileSync(STATE_PATH, JSON.stringify({}, null, 2));
251 | 
252 | 		const { default: addTask } = await import(
253 | 			'../../../scripts/modules/task-manager/add-task.js'
254 | 		);
255 | 		const { resolveTag } = await import('../../../scripts/modules/utils.js');
256 | 
257 | 		const tag = resolveTag({ projectRoot: TEMP_DIR }); // should return master
258 | 
259 | 		await addTask(
260 | 			TASKS_PATH,
261 | 			'Fallback task',
262 | 			[],
263 | 			null,
264 | 			{ projectRoot: TEMP_DIR, tag },
265 | 			'json',
266 | 			{
267 | 				title: 'Fallback',
268 | 				description: '-',
269 | 				details: '-',
270 | 				testStrategy: '-'
271 | 			},
272 | 			false
273 | 		);
274 | 
275 | 		const data = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
276 | 		expect(data.master.tasks.length).toBe(2); // seed + new task
277 | 	});
278 | });
279 | 
```

--------------------------------------------------------------------------------
/scripts/test-claude.js:
--------------------------------------------------------------------------------

```javascript
  1 | #!/usr/bin/env node
  2 | 
  3 | /**
  4 |  * test-claude.js
  5 |  *
  6 |  * A simple test script to verify the improvements to the callClaude function.
  7 |  * This script tests different scenarios:
  8 |  * 1. Normal operation with a small PRD
  9 |  * 2. Testing with a large number of tasks (to potentially trigger task reduction)
 10 |  * 3. Simulating a failure to test retry logic
 11 |  */
 12 | 
 13 | import fs from 'fs';
 14 | import path from 'path';
 15 | import dotenv from 'dotenv';
 16 | import { fileURLToPath } from 'url';
 17 | import { dirname } from 'path';
 18 | 
 19 | const __filename = fileURLToPath(import.meta.url);
 20 | const __dirname = dirname(__filename);
 21 | 
 22 | // Load environment variables from .env file
 23 | dotenv.config();
 24 | 
 25 | // Create a simple PRD for testing
 26 | const createTestPRD = (size = 'small', taskComplexity = 'simple') => {
 27 | 	let content = `# Test PRD - ${size.toUpperCase()} SIZE, ${taskComplexity.toUpperCase()} COMPLEXITY\n\n`;
 28 | 
 29 | 	// Add more content based on size
 30 | 	if (size === 'small') {
 31 | 		content += `
 32 | ## Overview
 33 | This is a small test PRD to verify the callClaude function improvements.
 34 | 
 35 | ## Requirements
 36 | 1. Create a simple web application
 37 | 2. Implement user authentication
 38 | 3. Add a dashboard for users
 39 | 4. Create an admin panel
 40 | 5. Implement data visualization
 41 | 
 42 | ## Technical Stack
 43 | - Frontend: React
 44 | - Backend: Node.js
 45 | - Database: MongoDB
 46 | `;
 47 | 	} else if (size === 'medium') {
 48 | 		// Medium-sized PRD with more requirements
 49 | 		content += `
 50 | ## Overview
 51 | This is a medium-sized test PRD to verify the callClaude function improvements.
 52 | 
 53 | ## Requirements
 54 | 1. Create a web application with multiple pages
 55 | 2. Implement user authentication with OAuth
 56 | 3. Add a dashboard for users with customizable widgets
 57 | 4. Create an admin panel with user management
 58 | 5. Implement data visualization with charts and graphs
 59 | 6. Add real-time notifications
 60 | 7. Implement a search feature
 61 | 8. Add user profile management
 62 | 9. Implement role-based access control
 63 | 10. Add a reporting system
 64 | 11. Implement file uploads and management
 65 | 12. Add a commenting system
 66 | 13. Implement a rating system
 67 | 14. Add a recommendation engine
 68 | 15. Implement a payment system
 69 | 
 70 | ## Technical Stack
 71 | - Frontend: React with TypeScript
 72 | - Backend: Node.js with Express
 73 | - Database: MongoDB with Mongoose
 74 | - Authentication: JWT and OAuth
 75 | - Deployment: Docker and Kubernetes
 76 | - CI/CD: GitHub Actions
 77 | - Monitoring: Prometheus and Grafana
 78 | `;
 79 | 	} else if (size === 'large') {
 80 | 		// Large PRD with many requirements
 81 | 		content += `
 82 | ## Overview
 83 | This is a large test PRD to verify the callClaude function improvements.
 84 | 
 85 | ## Requirements
 86 | `;
 87 | 		// Generate 30 requirements
 88 | 		for (let i = 1; i <= 30; i++) {
 89 | 			content += `${i}. Requirement ${i} - This is a detailed description of requirement ${i}.\n`;
 90 | 		}
 91 | 
 92 | 		content += `
 93 | ## Technical Stack
 94 | - Frontend: React with TypeScript
 95 | - Backend: Node.js with Express
 96 | - Database: MongoDB with Mongoose
 97 | - Authentication: JWT and OAuth
 98 | - Deployment: Docker and Kubernetes
 99 | - CI/CD: GitHub Actions
100 | - Monitoring: Prometheus and Grafana
101 | 
102 | ## User Stories
103 | `;
104 | 		// Generate 20 user stories
105 | 		for (let i = 1; i <= 20; i++) {
106 | 			content += `- As a user, I want to be able to ${i} so that I can achieve benefit ${i}.\n`;
107 | 		}
108 | 
109 | 		content += `
110 | ## Non-Functional Requirements
111 | - Performance: The system should respond within 200ms
112 | - Scalability: The system should handle 10,000 concurrent users
113 | - Availability: The system should have 99.9% uptime
114 | - Security: The system should comply with OWASP top 10
115 | - Accessibility: The system should comply with WCAG 2.1 AA
116 | `;
117 | 	}
118 | 
119 | 	// Add complexity if needed
120 | 	if (taskComplexity === 'complex') {
121 | 		content += `
122 | ## Complex Requirements
123 | - Implement a real-time collaboration system
124 | - Add a machine learning-based recommendation engine
125 | - Implement a distributed caching system
126 | - Add a microservices architecture
127 | - Implement a custom analytics engine
128 | - Add support for multiple languages and locales
129 | - Implement a custom search engine with advanced filtering
130 | - Add a custom workflow engine
131 | - Implement a custom reporting system
132 | - Add a custom dashboard builder
133 | `;
134 | 	}
135 | 
136 | 	return content;
137 | };
138 | 
139 | // Function to run the tests
140 | async function runTests() {
141 | 	console.log('Starting tests for callClaude function improvements...');
142 | 
143 | 	try {
144 | 		// Instead of importing the callClaude function directly, we'll use the dev.js script
145 | 		// with our test PRDs by running it as a child process
146 | 
147 | 		// Test 1: Small PRD, 5 tasks
148 | 		console.log('\n=== Test 1: Small PRD, 5 tasks ===');
149 | 		const smallPRD = createTestPRD('small', 'simple');
150 | 		const smallPRDPath = path.join(__dirname, 'test-small-prd.txt');
151 | 		fs.writeFileSync(smallPRDPath, smallPRD, 'utf8');
152 | 
153 | 		console.log(`Created test PRD at ${smallPRDPath}`);
154 | 		console.log('Running dev.js with small PRD...');
155 | 
156 | 		// Use the child_process module to run the dev.js script
157 | 		const { execSync } = await import('child_process');
158 | 
159 | 		try {
160 | 			const smallResult = execSync(
161 | 				`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${smallPRDPath} --num-tasks=5`,
162 | 				{
163 | 					stdio: 'inherit'
164 | 				}
165 | 			);
166 | 			console.log('Small PRD test completed successfully');
167 | 		} catch (error) {
168 | 			console.error('Small PRD test failed:', error.message);
169 | 		}
170 | 
171 | 		// Test 2: Medium PRD, 15 tasks
172 | 		console.log('\n=== Test 2: Medium PRD, 15 tasks ===');
173 | 		const mediumPRD = createTestPRD('medium', 'simple');
174 | 		const mediumPRDPath = path.join(__dirname, 'test-medium-prd.txt');
175 | 		fs.writeFileSync(mediumPRDPath, mediumPRD, 'utf8');
176 | 
177 | 		console.log(`Created test PRD at ${mediumPRDPath}`);
178 | 		console.log('Running dev.js with medium PRD...');
179 | 
180 | 		try {
181 | 			const mediumResult = execSync(
182 | 				`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${mediumPRDPath} --num-tasks=15`,
183 | 				{
184 | 					stdio: 'inherit'
185 | 				}
186 | 			);
187 | 			console.log('Medium PRD test completed successfully');
188 | 		} catch (error) {
189 | 			console.error('Medium PRD test failed:', error.message);
190 | 		}
191 | 
192 | 		// Test 3: Large PRD, 25 tasks
193 | 		console.log('\n=== Test 3: Large PRD, 25 tasks ===');
194 | 		const largePRD = createTestPRD('large', 'complex');
195 | 		const largePRDPath = path.join(__dirname, 'test-large-prd.txt');
196 | 		fs.writeFileSync(largePRDPath, largePRD, 'utf8');
197 | 
198 | 		console.log(`Created test PRD at ${largePRDPath}`);
199 | 		console.log('Running dev.js with large PRD...');
200 | 
201 | 		try {
202 | 			const largeResult = execSync(
203 | 				`node ${path.join(__dirname, 'dev.js')} parse-prd --input=${largePRDPath} --num-tasks=25`,
204 | 				{
205 | 					stdio: 'inherit'
206 | 				}
207 | 			);
208 | 			console.log('Large PRD test completed successfully');
209 | 		} catch (error) {
210 | 			console.error('Large PRD test failed:', error.message);
211 | 		}
212 | 
213 | 		console.log('\nAll tests completed!');
214 | 	} catch (error) {
215 | 		console.error('Test failed:', error);
216 | 	} finally {
217 | 		// Clean up test files
218 | 		console.log('\nCleaning up test files...');
219 | 		const testFiles = [
220 | 			path.join(__dirname, 'test-small-prd.txt'),
221 | 			path.join(__dirname, 'test-medium-prd.txt'),
222 | 			path.join(__dirname, 'test-large-prd.txt')
223 | 		];
224 | 
225 | 		testFiles.forEach((file) => {
226 | 			if (fs.existsSync(file)) {
227 | 				fs.unlinkSync(file);
228 | 				console.log(`Deleted ${file}`);
229 | 			}
230 | 		});
231 | 
232 | 		console.log('Cleanup complete.');
233 | 	}
234 | }
235 | 
236 | // Run the tests
237 | runTests().catch((error) => {
238 | 	console.error('Error running tests:', error);
239 | 	process.exit(1);
240 | });
241 | 
```

--------------------------------------------------------------------------------
/packages/claude-code-plugin/agents/task-orchestrator.md:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | name: task-orchestrator
  3 | 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>
  4 | model: opus
  5 | color: green
  6 | ---
  7 | 
  8 | 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.
  9 | 
 10 | ## Core Responsibilities
 11 | 
 12 | 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.
 13 | 
 14 | 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.
 15 | 
 16 | 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.
 17 | 
 18 | 4. **Progress Coordination**: You track the progress of deployed executors, handle task completion notifications, and reassess the execution strategy as tasks complete.
 19 | 
 20 | ## Operational Workflow
 21 | 
 22 | ### Initial Assessment Phase
 23 | 1. Use `get_tasks` or `task-master list` to retrieve all available tasks
 24 | 2. Analyze task statuses, priorities, and dependencies
 25 | 3. Identify tasks with status 'pending' that have no blocking dependencies
 26 | 4. Group related tasks that could benefit from specialized executors
 27 | 5. Create an execution plan that maximizes parallelization
 28 | 
 29 | ### Executor Deployment Phase
 30 | 1. For each independent task or task group:
 31 |    - Deploy a task-executor agent with specific instructions
 32 |    - Provide the executor with task ID, requirements, and context
 33 |    - Set clear completion criteria and reporting expectations
 34 | 2. Maintain a registry of active executors and their assigned tasks
 35 | 3. Establish communication protocols for progress updates
 36 | 
 37 | ### Coordination Phase
 38 | 1. Monitor executor progress through task status updates
 39 | 2. When a task completes:
 40 |    - Verify completion with `get_task` or `task-master show <id>`
 41 |    - Update task status if needed using `set_task_status`
 42 |    - Reassess dependency graph for newly unblocked tasks
 43 |    - Deploy new executors for available work
 44 | 3. Handle executor failures or blocks:
 45 |    - Reassign tasks to new executors if needed
 46 |    - Escalate complex issues to the user
 47 |    - Update task status to 'blocked' when appropriate
 48 | 
 49 | ### Optimization Strategies
 50 | 
 51 | **Parallel Execution Rules**:
 52 | - Never assign dependent tasks to different executors simultaneously
 53 | - Prioritize high-priority tasks when resources are limited
 54 | - Group small, related subtasks for single executor efficiency
 55 | - Balance executor load to prevent bottlenecks
 56 | 
 57 | **Context Management**:
 58 | - Provide executors with minimal but sufficient context
 59 | - Share relevant completed task information when it aids execution
 60 | - Maintain a shared knowledge base of project-specific patterns
 61 | 
 62 | **Quality Assurance**:
 63 | - Verify task completion before marking as done
 64 | - Ensure test strategies are followed when specified
 65 | - Coordinate cross-task integration testing when needed
 66 | 
 67 | ## Communication Protocols
 68 | 
 69 | When deploying executors, provide them with:
 70 | ```
 71 | TASK ASSIGNMENT:
 72 | - Task ID: [specific ID]
 73 | - Objective: [clear goal]
 74 | - Dependencies: [list any completed prerequisites]
 75 | - Success Criteria: [specific completion requirements]
 76 | - Context: [relevant project information]
 77 | - Reporting: [when and how to report back]
 78 | ```
 79 | 
 80 | When receiving executor updates:
 81 | 1. Acknowledge completion or issues
 82 | 2. Update task status in Task Master
 83 | 3. Reassess execution strategy
 84 | 4. Deploy new executors as appropriate
 85 | 
 86 | ## Decision Framework
 87 | 
 88 | **When to parallelize**:
 89 | - Multiple pending tasks with no interdependencies
 90 | - Sufficient context available for independent execution
 91 | - Tasks are well-defined with clear success criteria
 92 | 
 93 | **When to serialize**:
 94 | - Strong dependencies between tasks
 95 | - Limited context or unclear requirements
 96 | - Integration points requiring careful coordination
 97 | 
 98 | **When to escalate**:
 99 | - Circular dependencies detected
100 | - Critical blockers affecting multiple tasks
101 | - Ambiguous requirements needing clarification
102 | - Resource conflicts between executors
103 | 
104 | ## Error Handling
105 | 
106 | 1. **Executor Failure**: Reassign task to new executor with additional context about the failure
107 | 2. **Dependency Conflicts**: Halt affected executors, resolve conflict, then resume
108 | 3. **Task Ambiguity**: Request clarification from user before proceeding
109 | 4. **System Errors**: Implement graceful degradation, falling back to serial execution if needed
110 | 
111 | ## Performance Metrics
112 | 
113 | Track and optimize for:
114 | - Task completion rate
115 | - Parallel execution efficiency
116 | - Executor success rate
117 | - Time to completion for task groups
118 | - Dependency resolution speed
119 | 
120 | ## Integration with Task Master
121 | 
122 | Leverage these Task Master MCP tools effectively:
123 | - `get_tasks` - Continuous queue monitoring
124 | - `get_task` - Detailed task analysis
125 | - `set_task_status` - Progress tracking
126 | - `next_task` - Fallback for serial execution
127 | - `analyze_project_complexity` - Strategic planning
128 | - `complexity_report` - Resource allocation
129 | 
130 | 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.
131 | 
```

--------------------------------------------------------------------------------
/tests/unit/scripts/modules/dependency-manager/circular-dependencies.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | import {
  3 | 	validateCrossTagMove,
  4 | 	findCrossTagDependencies,
  5 | 	getDependentTaskIds,
  6 | 	validateSubtaskMove,
  7 | 	canMoveWithDependencies
  8 | } from '../../../../../scripts/modules/dependency-manager.js';
  9 | 
 10 | describe('Circular Dependency Scenarios', () => {
 11 | 	describe('Circular Cross-Tag Dependencies', () => {
 12 | 		const allTasks = [
 13 | 			{
 14 | 				id: 1,
 15 | 				title: 'Task 1',
 16 | 				dependencies: [2],
 17 | 				status: 'pending',
 18 | 				tag: 'backlog'
 19 | 			},
 20 | 			{
 21 | 				id: 2,
 22 | 				title: 'Task 2',
 23 | 				dependencies: [3],
 24 | 				status: 'pending',
 25 | 				tag: 'backlog'
 26 | 			},
 27 | 			{
 28 | 				id: 3,
 29 | 				title: 'Task 3',
 30 | 				dependencies: [1],
 31 | 				status: 'pending',
 32 | 				tag: 'backlog'
 33 | 			}
 34 | 		];
 35 | 
 36 | 		it('should detect circular dependencies across tags', () => {
 37 | 			// Task 1 depends on 2, 2 depends on 3, 3 depends on 1 (circular)
 38 | 			// But since all tasks are in 'backlog' and target is 'in-progress',
 39 | 			// only direct dependencies that are in different tags will be found
 40 | 			const conflicts = findCrossTagDependencies(
 41 | 				[allTasks[0]],
 42 | 				'backlog',
 43 | 				'in-progress',
 44 | 				allTasks
 45 | 			);
 46 | 
 47 | 			// Only direct dependencies of task 1 that are not in target tag
 48 | 			expect(conflicts).toHaveLength(1);
 49 | 			expect(
 50 | 				conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
 51 | 			).toBe(true);
 52 | 		});
 53 | 
 54 | 		it('should block move with circular dependencies', () => {
 55 | 			// Since task 1 has dependencies in the same tag, validateCrossTagMove should not throw
 56 | 			// The function only checks direct dependencies, not circular chains
 57 | 			expect(() => {
 58 | 				validateCrossTagMove(allTasks[0], 'backlog', 'in-progress', allTasks);
 59 | 			}).not.toThrow();
 60 | 		});
 61 | 
 62 | 		it('should return canMove: false for circular dependencies', () => {
 63 | 			const result = canMoveWithDependencies(
 64 | 				'1',
 65 | 				'backlog',
 66 | 				'in-progress',
 67 | 				allTasks
 68 | 			);
 69 | 			expect(result.canMove).toBe(false);
 70 | 			expect(result.conflicts).toHaveLength(1);
 71 | 		});
 72 | 	});
 73 | 
 74 | 	describe('Complex Dependency Chains', () => {
 75 | 		const allTasks = [
 76 | 			{
 77 | 				id: 1,
 78 | 				title: 'Task 1',
 79 | 				dependencies: [2, 3],
 80 | 				status: 'pending',
 81 | 				tag: 'backlog'
 82 | 			},
 83 | 			{
 84 | 				id: 2,
 85 | 				title: 'Task 2',
 86 | 				dependencies: [4],
 87 | 				status: 'pending',
 88 | 				tag: 'backlog'
 89 | 			},
 90 | 			{
 91 | 				id: 3,
 92 | 				title: 'Task 3',
 93 | 				dependencies: [5],
 94 | 				status: 'pending',
 95 | 				tag: 'backlog'
 96 | 			},
 97 | 			{
 98 | 				id: 4,
 99 | 				title: 'Task 4',
100 | 				dependencies: [],
101 | 				status: 'pending',
102 | 				tag: 'backlog'
103 | 			},
104 | 			{
105 | 				id: 5,
106 | 				title: 'Task 5',
107 | 				dependencies: [6],
108 | 				status: 'pending',
109 | 				tag: 'backlog'
110 | 			},
111 | 			{
112 | 				id: 6,
113 | 				title: 'Task 6',
114 | 				dependencies: [],
115 | 				status: 'pending',
116 | 				tag: 'backlog'
117 | 			},
118 | 			{
119 | 				id: 7,
120 | 				title: 'Task 7',
121 | 				dependencies: [],
122 | 				status: 'in-progress',
123 | 				tag: 'in-progress'
124 | 			}
125 | 		];
126 | 
127 | 		it('should find all dependencies in complex chain', () => {
128 | 			const conflicts = findCrossTagDependencies(
129 | 				[allTasks[0]],
130 | 				'backlog',
131 | 				'in-progress',
132 | 				allTasks
133 | 			);
134 | 
135 | 			// Only direct dependencies of task 1 that are not in target tag
136 | 			expect(conflicts).toHaveLength(2);
137 | 			expect(
138 | 				conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
139 | 			).toBe(true);
140 | 			expect(
141 | 				conflicts.some((c) => c.taskId === 1 && c.dependencyId === 3)
142 | 			).toBe(true);
143 | 		});
144 | 
145 | 		it('should get all dependent task IDs in complex chain', () => {
146 | 			const conflicts = findCrossTagDependencies(
147 | 				[allTasks[0]],
148 | 				'backlog',
149 | 				'in-progress',
150 | 				allTasks
151 | 			);
152 | 			const dependentIds = getDependentTaskIds(
153 | 				[allTasks[0]],
154 | 				conflicts,
155 | 				allTasks
156 | 			);
157 | 
158 | 			// Should include only the direct dependency IDs from conflicts
159 | 			expect(dependentIds).toContain(2);
160 | 			expect(dependentIds).toContain(3);
161 | 			// Should not include the source task or tasks not in conflicts
162 | 			expect(dependentIds).not.toContain(1);
163 | 		});
164 | 	});
165 | 
166 | 	describe('Mixed Dependency Types', () => {
167 | 		const allTasks = [
168 | 			{
169 | 				id: 1,
170 | 				title: 'Task 1',
171 | 				dependencies: [2, '3.1'],
172 | 				status: 'pending',
173 | 				tag: 'backlog'
174 | 			},
175 | 			{
176 | 				id: 2,
177 | 				title: 'Task 2',
178 | 				dependencies: [4],
179 | 				status: 'pending',
180 | 				tag: 'backlog'
181 | 			},
182 | 			{
183 | 				id: 3,
184 | 				title: 'Task 3',
185 | 				dependencies: [5],
186 | 				status: 'pending',
187 | 				tag: 'backlog',
188 | 				subtasks: [
189 | 					{
190 | 						id: 1,
191 | 						title: 'Subtask 3.1',
192 | 						dependencies: [],
193 | 						status: 'pending',
194 | 						tag: 'backlog'
195 | 					}
196 | 				]
197 | 			},
198 | 			{
199 | 				id: 4,
200 | 				title: 'Task 4',
201 | 				dependencies: [],
202 | 				status: 'pending',
203 | 				tag: 'backlog'
204 | 			},
205 | 			{
206 | 				id: 5,
207 | 				title: 'Task 5',
208 | 				dependencies: [],
209 | 				status: 'pending',
210 | 				tag: 'backlog'
211 | 			}
212 | 		];
213 | 
214 | 		it('should handle mixed task and subtask dependencies', () => {
215 | 			const conflicts = findCrossTagDependencies(
216 | 				[allTasks[0]],
217 | 				'backlog',
218 | 				'in-progress',
219 | 				allTasks
220 | 			);
221 | 
222 | 			expect(conflicts).toHaveLength(2);
223 | 			expect(
224 | 				conflicts.some((c) => c.taskId === 1 && c.dependencyId === 2)
225 | 			).toBe(true);
226 | 			expect(
227 | 				conflicts.some((c) => c.taskId === 1 && c.dependencyId === '3.1')
228 | 			).toBe(true);
229 | 		});
230 | 	});
231 | 
232 | 	describe('Large Task Set Performance', () => {
233 | 		const allTasks = [];
234 | 		for (let i = 1; i <= 100; i++) {
235 | 			allTasks.push({
236 | 				id: i,
237 | 				title: `Task ${i}`,
238 | 				dependencies: i < 100 ? [i + 1] : [],
239 | 				status: 'pending',
240 | 				tag: 'backlog'
241 | 			});
242 | 		}
243 | 
244 | 		it('should handle large task sets efficiently', () => {
245 | 			const conflicts = findCrossTagDependencies(
246 | 				[allTasks[0]],
247 | 				'backlog',
248 | 				'in-progress',
249 | 				allTasks
250 | 			);
251 | 
252 | 			expect(conflicts.length).toBeGreaterThan(0);
253 | 			expect(conflicts[0]).toHaveProperty('taskId');
254 | 			expect(conflicts[0]).toHaveProperty('dependencyId');
255 | 		});
256 | 	});
257 | 
258 | 	describe('Edge Cases and Error Conditions', () => {
259 | 		const allTasks = [
260 | 			{
261 | 				id: 1,
262 | 				title: 'Task 1',
263 | 				dependencies: [2],
264 | 				status: 'pending',
265 | 				tag: 'backlog'
266 | 			},
267 | 			{
268 | 				id: 2,
269 | 				title: 'Task 2',
270 | 				dependencies: [],
271 | 				status: 'pending',
272 | 				tag: 'backlog'
273 | 			}
274 | 		];
275 | 
276 | 		it('should handle empty task arrays', () => {
277 | 			expect(() => {
278 | 				findCrossTagDependencies([], 'backlog', 'in-progress', allTasks);
279 | 			}).not.toThrow();
280 | 		});
281 | 
282 | 		it('should handle non-existent tasks gracefully', () => {
283 | 			expect(() => {
284 | 				findCrossTagDependencies(
285 | 					[{ id: 999, dependencies: [] }],
286 | 					'backlog',
287 | 					'in-progress',
288 | 					allTasks
289 | 				);
290 | 			}).not.toThrow();
291 | 		});
292 | 
293 | 		it('should handle invalid tag names', () => {
294 | 			expect(() => {
295 | 				findCrossTagDependencies(
296 | 					[allTasks[0]],
297 | 					'invalid-tag',
298 | 					'in-progress',
299 | 					allTasks
300 | 				);
301 | 			}).not.toThrow();
302 | 		});
303 | 
304 | 		it('should handle null/undefined dependencies', () => {
305 | 			const taskWithNullDeps = {
306 | 				...allTasks[0],
307 | 				dependencies: [null, undefined, 2]
308 | 			};
309 | 			expect(() => {
310 | 				findCrossTagDependencies(
311 | 					[taskWithNullDeps],
312 | 					'backlog',
313 | 					'in-progress',
314 | 					allTasks
315 | 				);
316 | 			}).not.toThrow();
317 | 		});
318 | 
319 | 		it('should handle string dependencies correctly', () => {
320 | 			const taskWithStringDeps = { ...allTasks[0], dependencies: ['2', '3'] };
321 | 			const conflicts = findCrossTagDependencies(
322 | 				[taskWithStringDeps],
323 | 				'backlog',
324 | 				'in-progress',
325 | 				allTasks
326 | 			);
327 | 			expect(conflicts.length).toBeGreaterThanOrEqual(0);
328 | 		});
329 | 	});
330 | });
331 | 
```

--------------------------------------------------------------------------------
/apps/cli/tests/unit/ui/dashboard.component.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Tests for dashboard component calculations
  3 |  * Bug fix: Cancelled tasks should be treated as complete
  4 |  */
  5 | 
  6 | import type { Task } from '@tm/core';
  7 | import { describe, expect, it } from 'vitest';
  8 | import {
  9 | 	type TaskStatistics,
 10 | 	calculateDependencyStatistics,
 11 | 	calculateSubtaskStatistics,
 12 | 	calculateTaskStatistics
 13 | } from '../../../src/ui/components/dashboard.component.js';
 14 | 
 15 | describe('dashboard.component - Bug Fix: Cancelled Tasks as Complete', () => {
 16 | 	describe('calculateTaskStatistics', () => {
 17 | 		it('should treat cancelled tasks as complete in percentage calculation', () => {
 18 | 			// Arrange: 14 done, 1 cancelled = 100% complete
 19 | 			const tasks: Task[] = [
 20 | 				...Array.from({ length: 14 }, (_, i) => ({
 21 | 					id: i + 1,
 22 | 					title: `Task ${i + 1}`,
 23 | 					status: 'done' as const,
 24 | 					dependencies: []
 25 | 				})),
 26 | 				{
 27 | 					id: 15,
 28 | 					title: 'Cancelled Task',
 29 | 					status: 'cancelled' as const,
 30 | 					dependencies: []
 31 | 				}
 32 | 			];
 33 | 
 34 | 			// Act
 35 | 			const stats = calculateTaskStatistics(tasks);
 36 | 
 37 | 			// Assert
 38 | 			expect(stats.total).toBe(15);
 39 | 			expect(stats.done).toBe(14);
 40 | 			expect(stats.cancelled).toBe(1);
 41 | 			expect(stats.completedCount).toBe(15); // done + cancelled
 42 | 			// BUG: Current code shows 93% (14/15), should be 100% (15/15)
 43 | 			expect(stats.completionPercentage).toBe(100);
 44 | 		});
 45 | 
 46 | 		it('should treat completed status as complete in percentage calculation', () => {
 47 | 			// Arrange: Mix of done, completed, cancelled
 48 | 			const tasks: Task[] = [
 49 | 				{
 50 | 					id: 1,
 51 | 					title: 'Done Task',
 52 | 					status: 'done' as const,
 53 | 					dependencies: []
 54 | 				},
 55 | 				{
 56 | 					id: 2,
 57 | 					title: 'Completed Task',
 58 | 					status: 'completed' as const,
 59 | 					dependencies: []
 60 | 				},
 61 | 				{
 62 | 					id: 3,
 63 | 					title: 'Cancelled Task',
 64 | 					status: 'cancelled' as const,
 65 | 					dependencies: []
 66 | 				},
 67 | 				{
 68 | 					id: 4,
 69 | 					title: 'Pending Task',
 70 | 					status: 'pending' as const,
 71 | 					dependencies: []
 72 | 				}
 73 | 			];
 74 | 
 75 | 			// Act
 76 | 			const stats = calculateTaskStatistics(tasks);
 77 | 
 78 | 			// Assert
 79 | 			expect(stats.total).toBe(4);
 80 | 			expect(stats.done).toBe(1);
 81 | 			expect(stats.cancelled).toBe(1);
 82 | 			expect(stats.completedCount).toBe(3); // done + completed + cancelled
 83 | 			// 3 complete out of 4 total = 75%
 84 | 			expect(stats.completionPercentage).toBe(75);
 85 | 		});
 86 | 
 87 | 		it('should show 100% completion when all tasks are cancelled', () => {
 88 | 			// Arrange
 89 | 			const tasks: Task[] = [
 90 | 				{
 91 | 					id: 1,
 92 | 					title: 'Cancelled 1',
 93 | 					status: 'cancelled' as const,
 94 | 					dependencies: []
 95 | 				},
 96 | 				{
 97 | 					id: 2,
 98 | 					title: 'Cancelled 2',
 99 | 					status: 'cancelled' as const,
100 | 					dependencies: []
101 | 				}
102 | 			];
103 | 
104 | 			// Act
105 | 			const stats = calculateTaskStatistics(tasks);
106 | 
107 | 			// Assert
108 | 			expect(stats.total).toBe(2);
109 | 			expect(stats.cancelled).toBe(2);
110 | 			expect(stats.completedCount).toBe(2); // All cancelled = all complete
111 | 			// BUG: Current code shows 0%, should be 100%
112 | 			expect(stats.completionPercentage).toBe(100);
113 | 		});
114 | 
115 | 		it('should show 0% completion when no tasks are complete', () => {
116 | 			// Arrange
117 | 			const tasks: Task[] = [
118 | 				{
119 | 					id: 1,
120 | 					title: 'Pending Task',
121 | 					status: 'pending' as const,
122 | 					dependencies: []
123 | 				},
124 | 				{
125 | 					id: 2,
126 | 					title: 'In Progress Task',
127 | 					status: 'in-progress' as const,
128 | 					dependencies: []
129 | 				}
130 | 			];
131 | 
132 | 			// Act
133 | 			const stats = calculateTaskStatistics(tasks);
134 | 
135 | 			// Assert
136 | 			expect(stats.completionPercentage).toBe(0);
137 | 		});
138 | 	});
139 | 
140 | 	describe('calculateSubtaskStatistics', () => {
141 | 		it('should treat cancelled subtasks as complete in percentage calculation', () => {
142 | 			// Arrange: Task with 3 done subtasks and 1 cancelled = 100%
143 | 			const tasks: Task[] = [
144 | 				{
145 | 					id: 1,
146 | 					title: 'Parent Task',
147 | 					status: 'in-progress' as const,
148 | 					dependencies: [],
149 | 					subtasks: [
150 | 						{ id: '1', title: 'Sub 1', status: 'done' },
151 | 						{ id: '2', title: 'Sub 2', status: 'done' },
152 | 						{ id: '3', title: 'Sub 3', status: 'done' },
153 | 						{ id: '4', title: 'Sub 4', status: 'cancelled' }
154 | 					]
155 | 				}
156 | 			];
157 | 
158 | 			// Act
159 | 			const stats = calculateSubtaskStatistics(tasks);
160 | 
161 | 			// Assert
162 | 			expect(stats.total).toBe(4);
163 | 			expect(stats.done).toBe(3);
164 | 			expect(stats.cancelled).toBe(1);
165 | 			expect(stats.completedCount).toBe(4); // done + cancelled
166 | 			// BUG: Current code shows 75% (3/4), should be 100% (4/4)
167 | 			expect(stats.completionPercentage).toBe(100);
168 | 		});
169 | 
170 | 		it('should handle completed status in subtasks', () => {
171 | 			// Arrange
172 | 			const tasks: Task[] = [
173 | 				{
174 | 					id: 1,
175 | 					title: 'Parent Task',
176 | 					status: 'in-progress' as const,
177 | 					dependencies: [],
178 | 					subtasks: [
179 | 						{ id: '1', title: 'Sub 1', status: 'done' },
180 | 						{ id: '2', title: 'Sub 2', status: 'completed' },
181 | 						{ id: '3', title: 'Sub 3', status: 'pending' }
182 | 					]
183 | 				}
184 | 			];
185 | 
186 | 			// Act
187 | 			const stats = calculateSubtaskStatistics(tasks);
188 | 
189 | 			// Assert
190 | 			expect(stats.total).toBe(3);
191 | 			expect(stats.completedCount).toBe(2); // done + completed
192 | 			// 2 complete (done + completed) out of 3 = 67%
193 | 			expect(stats.completionPercentage).toBe(67);
194 | 		});
195 | 	});
196 | 
197 | 	describe('calculateDependencyStatistics', () => {
198 | 		it('should treat cancelled tasks as satisfied dependencies', () => {
199 | 			// Arrange: Task 15 depends on cancelled task 14
200 | 			const tasks: Task[] = [
201 | 				...Array.from({ length: 13 }, (_, i) => ({
202 | 					id: i + 1,
203 | 					title: `Task ${i + 1}`,
204 | 					status: 'done' as const,
205 | 					dependencies: []
206 | 				})),
207 | 				{
208 | 					id: 14,
209 | 					title: 'Cancelled Dependency',
210 | 					status: 'cancelled' as const,
211 | 					dependencies: []
212 | 				},
213 | 				{
214 | 					id: 15,
215 | 					title: 'Dependent Task',
216 | 					status: 'pending' as const,
217 | 					dependencies: [14]
218 | 				}
219 | 			];
220 | 
221 | 			// Act
222 | 			const stats = calculateDependencyStatistics(tasks);
223 | 
224 | 			// Assert
225 | 			// Task 15 should be ready to work on since its dependency (14) is cancelled
226 | 			// BUG: Current code shows task 15 as blocked, should show as ready
227 | 			expect(stats.tasksBlockedByDeps).toBe(0);
228 | 			expect(stats.tasksReadyToWork).toBeGreaterThan(0);
229 | 		});
230 | 
231 | 		it('should treat completed status as satisfied dependencies', () => {
232 | 			// Arrange
233 | 			const tasks: Task[] = [
234 | 				{
235 | 					id: 1,
236 | 					title: 'Completed Dependency',
237 | 					status: 'completed' as const,
238 | 					dependencies: []
239 | 				},
240 | 				{
241 | 					id: 2,
242 | 					title: 'Dependent Task',
243 | 					status: 'pending' as const,
244 | 					dependencies: [1]
245 | 				}
246 | 			];
247 | 
248 | 			// Act
249 | 			const stats = calculateDependencyStatistics(tasks);
250 | 
251 | 			// Assert
252 | 			expect(stats.tasksBlockedByDeps).toBe(0);
253 | 			expect(stats.tasksReadyToWork).toBe(1);
254 | 		});
255 | 
256 | 		it('should count tasks with cancelled dependencies as ready', () => {
257 | 			// Arrange: Multiple tasks depending on cancelled tasks
258 | 			const tasks: Task[] = [
259 | 				{
260 | 					id: 1,
261 | 					title: 'Cancelled Task',
262 | 					status: 'cancelled' as const,
263 | 					dependencies: []
264 | 				},
265 | 				{
266 | 					id: 2,
267 | 					title: 'Dependent 1',
268 | 					status: 'pending' as const,
269 | 					dependencies: [1]
270 | 				},
271 | 				{
272 | 					id: 3,
273 | 					title: 'Dependent 2',
274 | 					status: 'pending' as const,
275 | 					dependencies: [1]
276 | 				}
277 | 			];
278 | 
279 | 			// Act
280 | 			const stats = calculateDependencyStatistics(tasks);
281 | 
282 | 			// Assert
283 | 			expect(stats.tasksBlockedByDeps).toBe(0);
284 | 			expect(stats.tasksReadyToWork).toBe(2); // Both dependents should be ready
285 | 		});
286 | 	});
287 | });
288 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/config/managers/config-manager.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview Configuration Manager
  3 |  * Orchestrates configuration services following clean architecture principles
  4 |  *
  5 |  * This ConfigManager delegates responsibilities to specialized services for better
  6 |  * maintainability, testability, and separation of concerns.
  7 |  */
  8 | 
  9 | import type {
 10 | 	PartialConfiguration,
 11 | 	RuntimeStorageConfig
 12 | } from '../../../common/interfaces/configuration.interface.js';
 13 | import { DEFAULT_CONFIG_VALUES as DEFAULTS } from '../../../common/interfaces/configuration.interface.js';
 14 | import { ConfigLoader } from '../services/config-loader.service.js';
 15 | import {
 16 | 	CONFIG_PRECEDENCE,
 17 | 	ConfigMerger
 18 | } from '../services/config-merger.service.js';
 19 | import { ConfigPersistence } from '../services/config-persistence.service.js';
 20 | import { EnvironmentConfigProvider } from '../services/environment-config-provider.service.js';
 21 | import { RuntimeStateManager } from '../services/runtime-state-manager.service.js';
 22 | 
 23 | /**
 24 |  * ConfigManager orchestrates all configuration services
 25 |  *
 26 |  * This class delegates responsibilities to specialized services:
 27 |  * - ConfigLoader: Loads configuration from files
 28 |  * - ConfigMerger: Merges configurations with precedence
 29 |  * - RuntimeStateManager: Manages runtime state
 30 |  * - ConfigPersistence: Handles file persistence
 31 |  * - EnvironmentConfigProvider: Extracts env var configuration
 32 |  */
 33 | export class ConfigManager {
 34 | 	private projectRoot: string;
 35 | 	private config: PartialConfiguration = {};
 36 | 	private initialized = false;
 37 | 
 38 | 	// Services
 39 | 	private loader: ConfigLoader;
 40 | 	private merger: ConfigMerger;
 41 | 	private stateManager: RuntimeStateManager;
 42 | 	private persistence: ConfigPersistence;
 43 | 	private envProvider: EnvironmentConfigProvider;
 44 | 
 45 | 	/**
 46 | 	 * Create and initialize a new ConfigManager instance
 47 | 	 * This is the ONLY way to create a ConfigManager
 48 | 	 *
 49 | 	 * @param projectRoot - The root directory of the project
 50 | 	 * @returns Fully initialized ConfigManager instance
 51 | 	 */
 52 | 	static async create(projectRoot: string): Promise<ConfigManager> {
 53 | 		const manager = new ConfigManager(projectRoot);
 54 | 		await manager.initialize();
 55 | 		return manager;
 56 | 	}
 57 | 
 58 | 	/**
 59 | 	 * Private constructor - use ConfigManager.create() instead
 60 | 	 * This ensures the ConfigManager is always properly initialized
 61 | 	 */
 62 | 	private constructor(projectRoot: string) {
 63 | 		this.projectRoot = projectRoot;
 64 | 
 65 | 		// Initialize services
 66 | 		this.loader = new ConfigLoader(projectRoot);
 67 | 		this.merger = new ConfigMerger();
 68 | 		this.stateManager = new RuntimeStateManager(projectRoot);
 69 | 		this.persistence = new ConfigPersistence(projectRoot);
 70 | 		this.envProvider = new EnvironmentConfigProvider();
 71 | 	}
 72 | 
 73 | 	/**
 74 | 	 * Initialize by loading configuration from all sources
 75 | 	 * Private - only called by the factory method
 76 | 	 */
 77 | 	private async initialize(): Promise<void> {
 78 | 		if (this.initialized) return;
 79 | 
 80 | 		// Clear any existing configuration sources
 81 | 		this.merger.clearSources();
 82 | 
 83 | 		// 1. Load default configuration (lowest precedence)
 84 | 		this.merger.addSource({
 85 | 			name: 'defaults',
 86 | 			config: this.loader.getDefaultConfig(),
 87 | 			precedence: CONFIG_PRECEDENCE.DEFAULTS
 88 | 		});
 89 | 
 90 | 		// 2. Load global configuration (if exists)
 91 | 		const globalConfig = await this.loader.loadGlobalConfig();
 92 | 		if (globalConfig) {
 93 | 			this.merger.addSource({
 94 | 				name: 'global',
 95 | 				config: globalConfig,
 96 | 				precedence: CONFIG_PRECEDENCE.GLOBAL
 97 | 			});
 98 | 		}
 99 | 
100 | 		// 3. Load local project configuration
101 | 		const localConfig = await this.loader.loadLocalConfig();
102 | 		if (localConfig) {
103 | 			this.merger.addSource({
104 | 				name: 'local',
105 | 				config: localConfig,
106 | 				precedence: CONFIG_PRECEDENCE.LOCAL
107 | 			});
108 | 		}
109 | 
110 | 		// 4. Load environment variables (highest precedence)
111 | 		const envConfig = this.envProvider.loadConfig();
112 | 		if (Object.keys(envConfig).length > 0) {
113 | 			this.merger.addSource({
114 | 				name: 'environment',
115 | 				config: envConfig,
116 | 				precedence: CONFIG_PRECEDENCE.ENVIRONMENT
117 | 			});
118 | 		}
119 | 
120 | 		// 5. Merge all configurations
121 | 		this.config = this.merger.merge();
122 | 
123 | 		// 6. Load runtime state
124 | 		await this.stateManager.loadState();
125 | 
126 | 		this.initialized = true;
127 | 	}
128 | 
129 | 	// ==================== Configuration Access ====================
130 | 
131 | 	/**
132 | 	 * Get full configuration
133 | 	 */
134 | 	getConfig(): PartialConfiguration {
135 | 		return this.config;
136 | 	}
137 | 
138 | 	/**
139 | 	 * Get storage configuration
140 | 	 */
141 | 	getStorageConfig(): RuntimeStorageConfig {
142 | 		const storage = this.config.storage;
143 | 
144 | 		// Return the configured type (including 'auto')
145 | 		const storageType = storage?.type || 'auto';
146 | 		const basePath = storage?.basePath ?? this.projectRoot;
147 | 
148 | 		if (storageType === 'api' || storageType === 'auto') {
149 | 			return {
150 | 				type: storageType,
151 | 				basePath,
152 | 				apiEndpoint: storage?.apiEndpoint,
153 | 				apiAccessToken: storage?.apiAccessToken,
154 | 				apiConfigured: Boolean(storage?.apiEndpoint || storage?.apiAccessToken)
155 | 			};
156 | 		}
157 | 
158 | 		return {
159 | 			type: storageType,
160 | 			basePath,
161 | 			apiConfigured: false
162 | 		};
163 | 	}
164 | 
165 | 	/**
166 | 	 * Get model configuration
167 | 	 */
168 | 	getModelConfig() {
169 | 		return (
170 | 			this.config.models || {
171 | 				main: DEFAULTS.MODELS.MAIN,
172 | 				fallback: DEFAULTS.MODELS.FALLBACK
173 | 			}
174 | 		);
175 | 	}
176 | 
177 | 	/**
178 | 	 * Get response language setting
179 | 	 */
180 | 	getResponseLanguage(): string {
181 | 		const customConfig = this.config.custom as any;
182 | 		return customConfig?.responseLanguage || 'English';
183 | 	}
184 | 
185 | 	/**
186 | 	 * Get project root path
187 | 	 */
188 | 	getProjectRoot(): string {
189 | 		return this.projectRoot;
190 | 	}
191 | 
192 | 	/**
193 | 	 * Check if explicitly configured to use API storage
194 | 	 * Excludes 'auto' type
195 | 	 */
196 | 	isApiExplicitlyConfigured(): boolean {
197 | 		return this.getStorageConfig().type === 'api';
198 | 	}
199 | 
200 | 	// ==================== Runtime State ====================
201 | 
202 | 	/**
203 | 	 * Get the currently active tag
204 | 	 */
205 | 	getActiveTag(): string {
206 | 		return this.stateManager.getCurrentTag();
207 | 	}
208 | 
209 | 	/**
210 | 	 * Set the active tag
211 | 	 */
212 | 	async setActiveTag(tag: string): Promise<void> {
213 | 		await this.stateManager.setCurrentTag(tag);
214 | 	}
215 | 
216 | 	// ==================== Configuration Updates ====================
217 | 
218 | 	/**
219 | 	 * Update configuration
220 | 	 */
221 | 	async updateConfig(updates: PartialConfiguration): Promise<void> {
222 | 		// Merge updates into current config
223 | 		Object.assign(this.config, updates);
224 | 
225 | 		// Save to persistence
226 | 		await this.persistence.saveConfig(this.config);
227 | 
228 | 		// Re-initialize to respect precedence
229 | 		this.initialized = false;
230 | 		await this.initialize();
231 | 	}
232 | 
233 | 	/**
234 | 	 * Set response language
235 | 	 */
236 | 	async setResponseLanguage(language: string): Promise<void> {
237 | 		if (!this.config.custom) {
238 | 			this.config.custom = {};
239 | 		}
240 | 		(this.config.custom as any).responseLanguage = language;
241 | 		await this.persistence.saveConfig(this.config);
242 | 	}
243 | 
244 | 	/**
245 | 	 * Save current configuration
246 | 	 */
247 | 	async saveConfig(): Promise<void> {
248 | 		await this.persistence.saveConfig(this.config, {
249 | 			createBackup: true,
250 | 			atomic: true
251 | 		});
252 | 	}
253 | 
254 | 	// ==================== Utilities ====================
255 | 
256 | 	/**
257 | 	 * Reset configuration to defaults
258 | 	 */
259 | 	async reset(): Promise<void> {
260 | 		// Clear configuration file
261 | 		await this.persistence.deleteConfig();
262 | 
263 | 		// Clear runtime state
264 | 		await this.stateManager.clearState();
265 | 
266 | 		// Reset internal state
267 | 		this.initialized = false;
268 | 		this.config = {};
269 | 
270 | 		// Re-initialize with defaults
271 | 		await this.initialize();
272 | 	}
273 | 
274 | 	/**
275 | 	 * Get configuration sources for debugging
276 | 	 */
277 | 	getConfigSources() {
278 | 		return this.merger.getSources();
279 | 	}
280 | }
281 | 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/modules/auth/services/supabase-session-storage.spec.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Tests for SupabaseSessionStorage
  3 |  * Verifies session persistence with steno atomic writes
  4 |  */
  5 | 
  6 | import { afterEach, beforeEach, describe, expect, it } from 'vitest';
  7 | import fs from 'fs/promises';
  8 | import fsSync from 'fs';
  9 | import os from 'os';
 10 | import path from 'path';
 11 | import { SupabaseSessionStorage } from './supabase-session-storage.js';
 12 | 
 13 | describe('SupabaseSessionStorage', () => {
 14 | 	let tempDir: string;
 15 | 	let sessionPath: string;
 16 | 
 17 | 	beforeEach(() => {
 18 | 		// Create unique temp directory for each test
 19 | 		tempDir = fsSync.mkdtempSync(path.join(os.tmpdir(), 'tm-session-test-'));
 20 | 		sessionPath = path.join(tempDir, 'session.json');
 21 | 	});
 22 | 
 23 | 	afterEach(() => {
 24 | 		// Clean up temp directory
 25 | 		if (fsSync.existsSync(tempDir)) {
 26 | 			fsSync.rmSync(tempDir, { recursive: true, force: true });
 27 | 		}
 28 | 	});
 29 | 
 30 | 	describe('persistence with steno', () => {
 31 | 		it('should immediately persist data to disk with setItem', async () => {
 32 | 			const storage = new SupabaseSessionStorage(sessionPath);
 33 | 
 34 | 			// Set a session token
 35 | 			const testSession = JSON.stringify({
 36 | 				access_token: 'test-token',
 37 | 				refresh_token: 'test-refresh'
 38 | 			});
 39 | 			await storage.setItem('sb-localhost-auth-token', testSession);
 40 | 
 41 | 			// Immediately verify file exists and is readable (without any delay)
 42 | 			expect(fsSync.existsSync(sessionPath)).toBe(true);
 43 | 
 44 | 			// Read directly from disk (simulating a new process)
 45 | 			const diskData = JSON.parse(fsSync.readFileSync(sessionPath, 'utf8'));
 46 | 			expect(diskData['sb-localhost-auth-token']).toBe(testSession);
 47 | 		});
 48 | 
 49 | 		it('should guarantee data is on disk before returning', async () => {
 50 | 			const storage = new SupabaseSessionStorage(sessionPath);
 51 | 
 52 | 			// Write large session data
 53 | 			const largeSession = JSON.stringify({
 54 | 				access_token: 'x'.repeat(10000),
 55 | 				refresh_token: 'y'.repeat(10000),
 56 | 				user: { id: 'test', email: '[email protected]' }
 57 | 			});
 58 | 
 59 | 			await storage.setItem('sb-localhost-auth-token', largeSession);
 60 | 
 61 | 			// Create a NEW storage instance (simulates separate CLI command)
 62 | 			const newStorage = new SupabaseSessionStorage(sessionPath);
 63 | 
 64 | 			// Should immediately read the persisted data
 65 | 			const retrieved = await newStorage.getItem('sb-localhost-auth-token');
 66 | 			expect(retrieved).toBe(largeSession);
 67 | 		});
 68 | 
 69 | 		it('should handle rapid sequential writes without data loss', async () => {
 70 | 			const storage = new SupabaseSessionStorage(sessionPath);
 71 | 
 72 | 			// Simulate rapid token updates (like during refresh)
 73 | 			for (let i = 0; i < 5; i++) {
 74 | 				const session = JSON.stringify({
 75 | 					access_token: `token-${i}`,
 76 | 					expires_at: Date.now() + i * 1000
 77 | 				});
 78 | 				await storage.setItem('sb-localhost-auth-token', session);
 79 | 
 80 | 				// Each write should be immediately readable
 81 | 				const newStorage = new SupabaseSessionStorage(sessionPath);
 82 | 				const retrieved = await newStorage.getItem('sb-localhost-auth-token');
 83 | 				expect(JSON.parse(retrieved!).access_token).toBe(`token-${i}`);
 84 | 			}
 85 | 		});
 86 | 	});
 87 | 
 88 | 	describe('atomic writes', () => {
 89 | 		it('should complete writes atomically without leaving temp files', async () => {
 90 | 			const storage = new SupabaseSessionStorage(sessionPath);
 91 | 
 92 | 			await storage.setItem('test-key', 'test-value');
 93 | 
 94 | 			// Final file should exist with correct content
 95 | 			expect(fsSync.existsSync(sessionPath)).toBe(true);
 96 | 			const diskData = JSON.parse(fsSync.readFileSync(sessionPath, 'utf8'));
 97 | 			expect(diskData['test-key']).toBe('test-value');
 98 | 
 99 | 			// No unexpected extra files should remain in directory
100 | 			const files = fsSync.readdirSync(path.dirname(sessionPath));
101 | 			expect(files).toEqual([path.basename(sessionPath)]);
102 | 		});
103 | 
104 | 		it('should maintain correct file permissions (0700 for directory)', async () => {
105 | 			const storage = new SupabaseSessionStorage(sessionPath);
106 | 
107 | 			await storage.setItem('test-key', 'test-value');
108 | 
109 | 			// Check that file exists
110 | 			expect(fsSync.existsSync(sessionPath)).toBe(true);
111 | 
112 | 			// Check directory has correct permissions
113 | 			const dir = path.dirname(sessionPath);
114 | 			const stats = fsSync.statSync(dir);
115 | 			const mode = stats.mode & 0o777;
116 | 
117 | 			// Directory should be readable/writable/executable by owner only (0700)
118 | 			expect(mode).toBe(0o700);
119 | 		});
120 | 	});
121 | 
122 | 	describe('basic operations', () => {
123 | 		it('should get and set items', async () => {
124 | 			const storage = new SupabaseSessionStorage(sessionPath);
125 | 
126 | 			await storage.setItem('key1', 'value1');
127 | 			expect(await storage.getItem('key1')).toBe('value1');
128 | 		});
129 | 
130 | 		it('should return null for non-existent items', async () => {
131 | 			const storage = new SupabaseSessionStorage(sessionPath);
132 | 
133 | 			expect(await storage.getItem('non-existent')).toBe(null);
134 | 		});
135 | 
136 | 		it('should remove items', async () => {
137 | 			const storage = new SupabaseSessionStorage(sessionPath);
138 | 
139 | 			await storage.setItem('key1', 'value1');
140 | 			expect(await storage.getItem('key1')).toBe('value1');
141 | 
142 | 			await storage.removeItem('key1');
143 | 			expect(await storage.getItem('key1')).toBe(null);
144 | 
145 | 			// Verify removed from disk
146 | 			const newStorage = new SupabaseSessionStorage(sessionPath);
147 | 			expect(await newStorage.getItem('key1')).toBe(null);
148 | 		});
149 | 	});
150 | 
151 | 	describe('initialization', () => {
152 | 		it('should load existing session on initialization', async () => {
153 | 			// Create initial storage and save data
154 | 			const storage1 = new SupabaseSessionStorage(sessionPath);
155 | 			await storage1.setItem('key1', 'value1');
156 | 
157 | 			// Create new instance (simulates new process)
158 | 			const storage2 = new SupabaseSessionStorage(sessionPath);
159 | 			expect(await storage2.getItem('key1')).toBe('value1');
160 | 		});
161 | 
162 | 		it('should handle non-existent session file gracefully', async () => {
163 | 			// Don't create any file, just initialize
164 | 			const storage = new SupabaseSessionStorage(sessionPath);
165 | 
166 | 			// Should not throw and should work normally
167 | 			expect(await storage.getItem('any-key')).toBe(null);
168 | 			await storage.setItem('key1', 'value1');
169 | 			expect(await storage.getItem('key1')).toBe('value1');
170 | 		});
171 | 
172 | 		it('should create directory if it does not exist', async () => {
173 | 			const deepPath = path.join(
174 | 				tempDir,
175 | 				'deep',
176 | 				'nested',
177 | 				'path',
178 | 				'session.json'
179 | 			);
180 | 			const storage = new SupabaseSessionStorage(deepPath);
181 | 
182 | 			await storage.setItem('key1', 'value1');
183 | 
184 | 			expect(fsSync.existsSync(deepPath)).toBe(true);
185 | 			expect(fsSync.existsSync(path.dirname(deepPath))).toBe(true);
186 | 		});
187 | 	});
188 | 
189 | 	describe('error handling', () => {
190 | 		it('should not throw on persist errors', async () => {
191 | 			const invalidPath = '/invalid/path/that/cannot/be/written/session.json';
192 | 			const storage = new SupabaseSessionStorage(invalidPath);
193 | 
194 | 			// Should not throw, session should remain in memory
195 | 			await storage.setItem('key1', 'value1');
196 | 
197 | 			// Should still work in memory
198 | 			expect(await storage.getItem('key1')).toBe('value1');
199 | 		});
200 | 
201 | 		it('should handle corrupted session file gracefully', async () => {
202 | 			// Write corrupted JSON
203 | 			fsSync.writeFileSync(sessionPath, 'invalid json {{{');
204 | 
205 | 			// Should not throw on initialization
206 | 			expect(() => new SupabaseSessionStorage(sessionPath)).not.toThrow();
207 | 
208 | 			// Should work normally after initialization
209 | 			const storage = new SupabaseSessionStorage(sessionPath);
210 | 			await storage.setItem('key1', 'value1');
211 | 			expect(await storage.getItem('key1')).toBe('value1');
212 | 		});
213 | 	});
214 | });
215 | 
```

--------------------------------------------------------------------------------
/apps/mcp/src/shared/utils.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * Shared utilities for MCP tools
  3 |  */
  4 | 
  5 | import type { ContentResult } from 'fastmcp';
  6 | import path from 'node:path';
  7 | import fs from 'node:fs';
  8 | import packageJson from '../../../../package.json' with { type: 'json' };
  9 | 
 10 | /**
 11 |  * Get version information
 12 |  */
 13 | export function getVersionInfo() {
 14 | 	return {
 15 | 		version: packageJson.version || 'unknown',
 16 | 		name: packageJson.name || 'task-master-ai'
 17 | 	};
 18 | }
 19 | 
 20 | /**
 21 |  * Get current tag for a project root
 22 |  */
 23 | export function getCurrentTag(projectRoot: string): string | null {
 24 | 	try {
 25 | 		// Try to read current tag from state.json
 26 | 		const stateJsonPath = path.join(projectRoot, '.taskmaster', 'state.json');
 27 | 
 28 | 		if (fs.existsSync(stateJsonPath)) {
 29 | 			const stateData = JSON.parse(fs.readFileSync(stateJsonPath, 'utf-8'));
 30 | 			return stateData.currentTag || 'master';
 31 | 		}
 32 | 
 33 | 		return 'master';
 34 | 	} catch {
 35 | 		return null;
 36 | 	}
 37 | }
 38 | 
 39 | /**
 40 |  * Handle API result with standardized error handling and response formatting
 41 |  * This provides a consistent response structure for all MCP tools
 42 |  */
 43 | export async function handleApiResult<T>(options: {
 44 | 	result: { success: boolean; data?: T; error?: { message: string } };
 45 | 	log?: any;
 46 | 	errorPrefix?: string;
 47 | 	projectRoot?: string;
 48 | 	tag?: string; // Optional tag/brief to use instead of reading from state.json
 49 | }): Promise<ContentResult> {
 50 | 	const {
 51 | 		result,
 52 | 		log,
 53 | 		errorPrefix = 'API error',
 54 | 		projectRoot,
 55 | 		tag: providedTag
 56 | 	} = options;
 57 | 
 58 | 	// Get version info for every response
 59 | 	const versionInfo = getVersionInfo();
 60 | 
 61 | 	// Use provided tag if available, otherwise get from state.json
 62 | 	// Note: For API storage, tm-core returns the brief name as the tag
 63 | 	const currentTag =
 64 | 		providedTag !== undefined
 65 | 			? providedTag
 66 | 			: projectRoot
 67 | 				? getCurrentTag(projectRoot)
 68 | 				: null;
 69 | 
 70 | 	if (!result.success) {
 71 | 		const errorMsg = result.error?.message || `Unknown ${errorPrefix}`;
 72 | 		log?.error?.(`${errorPrefix}: ${errorMsg}`);
 73 | 
 74 | 		let errorText = `Error: ${errorMsg}\nVersion: ${versionInfo.version}\nName: ${versionInfo.name}`;
 75 | 
 76 | 		if (currentTag) {
 77 | 			errorText += `\nCurrent Tag: ${currentTag}`;
 78 | 		}
 79 | 
 80 | 		return {
 81 | 			content: [
 82 | 				{
 83 | 					type: 'text',
 84 | 					text: errorText
 85 | 				}
 86 | 			],
 87 | 			isError: true
 88 | 		};
 89 | 	}
 90 | 
 91 | 	log?.info?.('Successfully completed operation');
 92 | 
 93 | 	// Create the response payload including version info and tag
 94 | 	const responsePayload: any = {
 95 | 		data: result.data,
 96 | 		version: versionInfo
 97 | 	};
 98 | 
 99 | 	// Add current tag if available
100 | 	if (currentTag) {
101 | 		responsePayload.tag = currentTag;
102 | 	}
103 | 
104 | 	return {
105 | 		content: [
106 | 			{
107 | 				type: 'text',
108 | 				text: JSON.stringify(responsePayload, null, 2)
109 | 			}
110 | 		]
111 | 	};
112 | }
113 | 
114 | /**
115 |  * Normalize project root path (handles URI encoding, file:// protocol, Windows paths)
116 |  */
117 | export function normalizeProjectRoot(rawPath: string): string {
118 | 	if (!rawPath) return process.cwd();
119 | 
120 | 	try {
121 | 		let pathString = rawPath;
122 | 
123 | 		// Decode URI encoding
124 | 		try {
125 | 			pathString = decodeURIComponent(pathString);
126 | 		} catch {
127 | 			// If decoding fails, use as-is
128 | 		}
129 | 
130 | 		// Strip file:// prefix
131 | 		if (pathString.startsWith('file:///')) {
132 | 			pathString = pathString.slice(7);
133 | 		} else if (pathString.startsWith('file://')) {
134 | 			pathString = pathString.slice(7);
135 | 		}
136 | 
137 | 		// Handle Windows drive letter after stripping prefix (e.g., /C:/...)
138 | 		if (
139 | 			pathString.startsWith('/') &&
140 | 			/[A-Za-z]:/.test(pathString.substring(1, 3))
141 | 		) {
142 | 			pathString = pathString.substring(1);
143 | 		}
144 | 
145 | 		// Normalize backslashes to forward slashes
146 | 		pathString = pathString.replace(/\\/g, '/');
147 | 
148 | 		// Resolve to absolute path
149 | 		return path.resolve(pathString);
150 | 	} catch {
151 | 		return path.resolve(rawPath);
152 | 	}
153 | }
154 | 
155 | /**
156 |  * Get project root from session object
157 |  */
158 | function getProjectRootFromSession(session: any): string | null {
159 | 	try {
160 | 		// Check primary location
161 | 		if (session?.roots?.[0]?.uri) {
162 | 			return normalizeProjectRoot(session.roots[0].uri);
163 | 		}
164 | 		// Check alternate location
165 | 		else if (session?.roots?.roots?.[0]?.uri) {
166 | 			return normalizeProjectRoot(session.roots.roots[0].uri);
167 | 		}
168 | 		return null;
169 | 	} catch {
170 | 		return null;
171 | 	}
172 | }
173 | 
174 | /**
175 |  * Wrapper to normalize project root in args with proper precedence order
176 |  *
177 |  * PRECEDENCE ORDER:
178 |  * 1. TASK_MASTER_PROJECT_ROOT environment variable (from process.env or session)
179 |  * 2. args.projectRoot (explicitly provided)
180 |  * 3. Session-based project root resolution
181 |  * 4. Current directory fallback
182 |  */
183 | export function withNormalizedProjectRoot<T extends { projectRoot?: string }>(
184 | 	fn: (
185 | 		args: T & { projectRoot: string },
186 | 		context: any
187 | 	) => Promise<ContentResult>
188 | ): (args: T, context: any) => Promise<ContentResult> {
189 | 	return async (args: T, context: any): Promise<ContentResult> => {
190 | 		const { log, session } = context;
191 | 		let normalizedRoot: string | null = null;
192 | 		let rootSource = 'unknown';
193 | 
194 | 		try {
195 | 			// 1. Check for TASK_MASTER_PROJECT_ROOT environment variable first
196 | 			if (process.env.TASK_MASTER_PROJECT_ROOT) {
197 | 				const envRoot = process.env.TASK_MASTER_PROJECT_ROOT;
198 | 				normalizedRoot = path.isAbsolute(envRoot)
199 | 					? envRoot
200 | 					: path.resolve(process.cwd(), envRoot);
201 | 				rootSource = 'TASK_MASTER_PROJECT_ROOT environment variable';
202 | 				log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
203 | 			}
204 | 			// Also check session environment variables for TASK_MASTER_PROJECT_ROOT
205 | 			else if (session?.env?.TASK_MASTER_PROJECT_ROOT) {
206 | 				const envRoot = session.env.TASK_MASTER_PROJECT_ROOT;
207 | 				normalizedRoot = path.isAbsolute(envRoot)
208 | 					? envRoot
209 | 					: path.resolve(process.cwd(), envRoot);
210 | 				rootSource = 'TASK_MASTER_PROJECT_ROOT session environment variable';
211 | 				log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
212 | 			}
213 | 			// 2. If no environment variable, try args.projectRoot
214 | 			else if (args.projectRoot) {
215 | 				normalizedRoot = normalizeProjectRoot(args.projectRoot);
216 | 				rootSource = 'args.projectRoot';
217 | 				log?.info?.(`Using project root from ${rootSource}: ${normalizedRoot}`);
218 | 			}
219 | 			// 3. If no args.projectRoot, try session-based resolution
220 | 			else {
221 | 				const sessionRoot = getProjectRootFromSession(session);
222 | 				if (sessionRoot) {
223 | 					normalizedRoot = sessionRoot;
224 | 					rootSource = 'session';
225 | 					log?.info?.(
226 | 						`Using project root from ${rootSource}: ${normalizedRoot}`
227 | 					);
228 | 				}
229 | 			}
230 | 
231 | 			if (!normalizedRoot) {
232 | 				log?.error?.(
233 | 					'Could not determine project root from environment, args, or session.'
234 | 				);
235 | 				return handleApiResult({
236 | 					result: {
237 | 						success: false,
238 | 						error: {
239 | 							message:
240 | 								'Could not determine project root. Please provide projectRoot argument or ensure TASK_MASTER_PROJECT_ROOT environment variable is set.'
241 | 						}
242 | 					}
243 | 				});
244 | 			}
245 | 
246 | 			// Inject the normalized root back into args
247 | 			const updatedArgs = { ...args, projectRoot: normalizedRoot } as T & {
248 | 				projectRoot: string;
249 | 			};
250 | 
251 | 			// Execute the original function with normalized root in args
252 | 			return await fn(updatedArgs, context);
253 | 		} catch (error: any) {
254 | 			log?.error?.(
255 | 				`Error within withNormalizedProjectRoot HOF (Normalized Root: ${normalizedRoot}): ${error.message}`
256 | 			);
257 | 			if (error.stack && log?.debug) {
258 | 				log.debug(error.stack);
259 | 			}
260 | 			return handleApiResult({
261 | 				result: {
262 | 					success: false,
263 | 					error: {
264 | 						message: `Operation failed: ${error.message}`
265 | 					}
266 | 				}
267 | 			});
268 | 		}
269 | 	};
270 | }
271 | 
```
Page 22/69FirstPrevNextLast