This is page 39 of 69. Use http://codebase.md/eyaltoledano/claude-task-master?lines=true&page={x} to view the full context.
# Directory Structure
```
├── .changeset
│ ├── config.json
│ └── README.md
├── .claude
│ ├── commands
│ │ └── dedupe.md
│ └── TM_COMMANDS_GUIDE.md
├── .claude-plugin
│ └── marketplace.json
├── .coderabbit.yaml
├── .cursor
│ ├── mcp.json
│ └── rules
│ ├── ai_providers.mdc
│ ├── ai_services.mdc
│ ├── architecture.mdc
│ ├── changeset.mdc
│ ├── commands.mdc
│ ├── context_gathering.mdc
│ ├── cursor_rules.mdc
│ ├── dependencies.mdc
│ ├── dev_workflow.mdc
│ ├── git_workflow.mdc
│ ├── glossary.mdc
│ ├── mcp.mdc
│ ├── new_features.mdc
│ ├── self_improve.mdc
│ ├── tags.mdc
│ ├── taskmaster.mdc
│ ├── tasks.mdc
│ ├── telemetry.mdc
│ ├── test_workflow.mdc
│ ├── tests.mdc
│ ├── ui.mdc
│ └── utilities.mdc
├── .cursorignore
├── .env.example
├── .github
│ ├── ISSUE_TEMPLATE
│ │ ├── bug_report.md
│ │ ├── enhancements---feature-requests.md
│ │ └── feedback.md
│ ├── PULL_REQUEST_TEMPLATE
│ │ ├── bugfix.md
│ │ ├── config.yml
│ │ ├── feature.md
│ │ └── integration.md
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── scripts
│ │ ├── auto-close-duplicates.mjs
│ │ ├── backfill-duplicate-comments.mjs
│ │ ├── check-pre-release-mode.mjs
│ │ ├── parse-metrics.mjs
│ │ ├── release.mjs
│ │ ├── tag-extension.mjs
│ │ ├── utils.mjs
│ │ └── validate-changesets.mjs
│ └── workflows
│ ├── auto-close-duplicates.yml
│ ├── backfill-duplicate-comments.yml
│ ├── ci.yml
│ ├── claude-dedupe-issues.yml
│ ├── claude-docs-trigger.yml
│ ├── claude-docs-updater.yml
│ ├── claude-issue-triage.yml
│ ├── claude.yml
│ ├── extension-ci.yml
│ ├── extension-release.yml
│ ├── log-issue-events.yml
│ ├── pre-release.yml
│ ├── release-check.yml
│ ├── release.yml
│ ├── update-models-md.yml
│ └── weekly-metrics-discord.yml
├── .gitignore
├── .kiro
│ ├── hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── settings
│ │ └── mcp.json
│ └── steering
│ ├── dev_workflow.md
│ ├── kiro_rules.md
│ ├── self_improve.md
│ ├── taskmaster_hooks_workflow.md
│ └── taskmaster.md
├── .manypkg.json
├── .mcp.json
├── .npmignore
├── .nvmrc
├── .taskmaster
│ ├── CLAUDE.md
│ ├── config.json
│ ├── docs
│ │ ├── autonomous-tdd-git-workflow.md
│ │ ├── MIGRATION-ROADMAP.md
│ │ ├── prd-tm-start.txt
│ │ ├── prd.txt
│ │ ├── README.md
│ │ ├── research
│ │ │ ├── 2025-06-14_how-can-i-improve-the-scope-up-and-scope-down-comm.md
│ │ │ ├── 2025-06-14_should-i-be-using-any-specific-libraries-for-this.md
│ │ │ ├── 2025-06-14_test-save-functionality.md
│ │ │ ├── 2025-06-14_test-the-fix-for-duplicate-saves-final-test.md
│ │ │ └── 2025-08-01_do-we-need-to-add-new-commands-or-can-we-just-weap.md
│ │ ├── task-template-importing-prd.txt
│ │ ├── tdd-workflow-phase-0-spike.md
│ │ ├── tdd-workflow-phase-1-core-rails.md
│ │ ├── tdd-workflow-phase-1-orchestrator.md
│ │ ├── tdd-workflow-phase-2-pr-resumability.md
│ │ ├── tdd-workflow-phase-3-extensibility-guardrails.md
│ │ ├── test-prd.txt
│ │ └── tm-core-phase-1.txt
│ ├── reports
│ │ ├── task-complexity-report_autonomous-tdd-git-workflow.json
│ │ ├── task-complexity-report_cc-kiro-hooks.json
│ │ ├── task-complexity-report_tdd-phase-1-core-rails.json
│ │ ├── task-complexity-report_tdd-workflow-phase-0.json
│ │ ├── task-complexity-report_test-prd-tag.json
│ │ ├── task-complexity-report_tm-core-phase-1.json
│ │ ├── task-complexity-report.json
│ │ └── tm-core-complexity.json
│ ├── state.json
│ ├── tasks
│ │ ├── task_001_tm-start.txt
│ │ ├── task_002_tm-start.txt
│ │ ├── task_003_tm-start.txt
│ │ ├── task_004_tm-start.txt
│ │ ├── task_007_tm-start.txt
│ │ └── tasks.json
│ └── templates
│ ├── example_prd_rpg.md
│ └── example_prd.md
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── apps
│ ├── cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ ├── command-registry.ts
│ │ │ ├── commands
│ │ │ │ ├── auth.command.ts
│ │ │ │ ├── autopilot
│ │ │ │ │ ├── abort.command.ts
│ │ │ │ │ ├── commit.command.ts
│ │ │ │ │ ├── complete.command.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next.command.ts
│ │ │ │ │ ├── resume.command.ts
│ │ │ │ │ ├── shared.ts
│ │ │ │ │ ├── start.command.ts
│ │ │ │ │ └── status.command.ts
│ │ │ │ ├── briefs.command.ts
│ │ │ │ ├── context.command.ts
│ │ │ │ ├── export.command.ts
│ │ │ │ ├── list.command.ts
│ │ │ │ ├── models
│ │ │ │ │ ├── custom-providers.ts
│ │ │ │ │ ├── fetchers.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── prompts.ts
│ │ │ │ │ ├── setup.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── next.command.ts
│ │ │ │ ├── set-status.command.ts
│ │ │ │ ├── show.command.ts
│ │ │ │ ├── start.command.ts
│ │ │ │ └── tags.command.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── model-management.ts
│ │ │ ├── types
│ │ │ │ └── tag-management.d.ts
│ │ │ ├── ui
│ │ │ │ ├── components
│ │ │ │ │ ├── cardBox.component.ts
│ │ │ │ │ ├── dashboard.component.ts
│ │ │ │ │ ├── header.component.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── next-task.component.ts
│ │ │ │ │ ├── suggested-steps.component.ts
│ │ │ │ │ └── task-detail.component.ts
│ │ │ │ ├── display
│ │ │ │ │ ├── messages.ts
│ │ │ │ │ └── tables.ts
│ │ │ │ ├── formatters
│ │ │ │ │ ├── complexity-formatters.ts
│ │ │ │ │ ├── dependency-formatters.ts
│ │ │ │ │ ├── priority-formatters.ts
│ │ │ │ │ ├── status-formatters.spec.ts
│ │ │ │ │ └── status-formatters.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── layout
│ │ │ │ ├── helpers.spec.ts
│ │ │ │ └── helpers.ts
│ │ │ └── utils
│ │ │ ├── auth-helpers.ts
│ │ │ ├── auto-update.ts
│ │ │ ├── brief-selection.ts
│ │ │ ├── display-helpers.ts
│ │ │ ├── error-handler.ts
│ │ │ ├── index.ts
│ │ │ ├── project-root.ts
│ │ │ ├── task-status.ts
│ │ │ ├── ui.spec.ts
│ │ │ └── ui.ts
│ │ ├── tests
│ │ │ ├── integration
│ │ │ │ └── commands
│ │ │ │ └── autopilot
│ │ │ │ └── workflow.test.ts
│ │ │ └── unit
│ │ │ ├── commands
│ │ │ │ ├── autopilot
│ │ │ │ │ └── shared.test.ts
│ │ │ │ ├── list.command.spec.ts
│ │ │ │ └── show.command.spec.ts
│ │ │ └── ui
│ │ │ └── dashboard.component.spec.ts
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ ├── docs
│ │ ├── archive
│ │ │ ├── ai-client-utils-example.mdx
│ │ │ ├── ai-development-workflow.mdx
│ │ │ ├── command-reference.mdx
│ │ │ ├── configuration.mdx
│ │ │ ├── cursor-setup.mdx
│ │ │ ├── examples.mdx
│ │ │ └── Installation.mdx
│ │ ├── best-practices
│ │ │ ├── advanced-tasks.mdx
│ │ │ ├── configuration-advanced.mdx
│ │ │ └── index.mdx
│ │ ├── capabilities
│ │ │ ├── cli-root-commands.mdx
│ │ │ ├── index.mdx
│ │ │ ├── mcp.mdx
│ │ │ ├── rpg-method.mdx
│ │ │ └── task-structure.mdx
│ │ ├── CHANGELOG.md
│ │ ├── command-reference.mdx
│ │ ├── configuration.mdx
│ │ ├── docs.json
│ │ ├── favicon.svg
│ │ ├── getting-started
│ │ │ ├── api-keys.mdx
│ │ │ ├── contribute.mdx
│ │ │ ├── faq.mdx
│ │ │ └── quick-start
│ │ │ ├── configuration-quick.mdx
│ │ │ ├── execute-quick.mdx
│ │ │ ├── installation.mdx
│ │ │ ├── moving-forward.mdx
│ │ │ ├── prd-quick.mdx
│ │ │ ├── quick-start.mdx
│ │ │ ├── requirements.mdx
│ │ │ ├── rules-quick.mdx
│ │ │ └── tasks-quick.mdx
│ │ ├── introduction.mdx
│ │ ├── licensing.md
│ │ ├── logo
│ │ │ ├── dark.svg
│ │ │ ├── light.svg
│ │ │ └── task-master-logo.png
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── style.css
│ │ ├── tdd-workflow
│ │ │ ├── ai-agent-integration.mdx
│ │ │ └── quickstart.mdx
│ │ ├── vercel.json
│ │ └── whats-new.mdx
│ ├── extension
│ │ ├── .vscodeignore
│ │ ├── assets
│ │ │ ├── banner.png
│ │ │ ├── icon-dark.svg
│ │ │ ├── icon-light.svg
│ │ │ ├── icon.png
│ │ │ ├── screenshots
│ │ │ │ ├── kanban-board.png
│ │ │ │ └── task-details.png
│ │ │ └── sidebar-icon.svg
│ │ ├── CHANGELOG.md
│ │ ├── components.json
│ │ ├── docs
│ │ │ ├── extension-CI-setup.md
│ │ │ └── extension-development-guide.md
│ │ ├── esbuild.js
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── package.mjs
│ │ ├── package.publish.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── components
│ │ │ │ ├── ConfigView.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── TaskDetails
│ │ │ │ │ ├── AIActionsSection.tsx
│ │ │ │ │ ├── DetailsSection.tsx
│ │ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ │ ├── SubtasksSection.tsx
│ │ │ │ │ ├── TaskMetadataSidebar.tsx
│ │ │ │ │ └── useTaskDetails.ts
│ │ │ │ ├── TaskDetailsView.tsx
│ │ │ │ ├── TaskMasterLogo.tsx
│ │ │ │ └── ui
│ │ │ │ ├── badge.tsx
│ │ │ │ ├── breadcrumb.tsx
│ │ │ │ ├── button.tsx
│ │ │ │ ├── card.tsx
│ │ │ │ ├── collapsible.tsx
│ │ │ │ ├── CollapsibleSection.tsx
│ │ │ │ ├── dropdown-menu.tsx
│ │ │ │ ├── label.tsx
│ │ │ │ ├── scroll-area.tsx
│ │ │ │ ├── separator.tsx
│ │ │ │ ├── shadcn-io
│ │ │ │ │ └── kanban
│ │ │ │ │ └── index.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── extension.ts
│ │ │ ├── index.ts
│ │ │ ├── lib
│ │ │ │ └── utils.ts
│ │ │ ├── services
│ │ │ │ ├── config-service.ts
│ │ │ │ ├── error-handler.ts
│ │ │ │ ├── notification-preferences.ts
│ │ │ │ ├── polling-service.ts
│ │ │ │ ├── polling-strategies.ts
│ │ │ │ ├── sidebar-webview-manager.ts
│ │ │ │ ├── task-repository.ts
│ │ │ │ ├── terminal-manager.ts
│ │ │ │ └── webview-manager.ts
│ │ │ ├── test
│ │ │ │ └── extension.test.ts
│ │ │ ├── utils
│ │ │ │ ├── configManager.ts
│ │ │ │ ├── connectionManager.ts
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── event-emitter.ts
│ │ │ │ ├── logger.ts
│ │ │ │ ├── mcpClient.ts
│ │ │ │ ├── notificationPreferences.ts
│ │ │ │ └── task-master-api
│ │ │ │ ├── cache
│ │ │ │ │ └── cache-manager.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── mcp-client.ts
│ │ │ │ ├── transformers
│ │ │ │ │ └── task-transformer.ts
│ │ │ │ └── types
│ │ │ │ └── index.ts
│ │ │ └── webview
│ │ │ ├── App.tsx
│ │ │ ├── components
│ │ │ │ ├── AppContent.tsx
│ │ │ │ ├── EmptyState.tsx
│ │ │ │ ├── ErrorBoundary.tsx
│ │ │ │ ├── PollingStatus.tsx
│ │ │ │ ├── PriorityBadge.tsx
│ │ │ │ ├── SidebarView.tsx
│ │ │ │ ├── TagDropdown.tsx
│ │ │ │ ├── TaskCard.tsx
│ │ │ │ ├── TaskEditModal.tsx
│ │ │ │ ├── TaskMasterKanban.tsx
│ │ │ │ ├── ToastContainer.tsx
│ │ │ │ └── ToastNotification.tsx
│ │ │ ├── constants
│ │ │ │ └── index.ts
│ │ │ ├── contexts
│ │ │ │ └── VSCodeContext.tsx
│ │ │ ├── hooks
│ │ │ │ ├── useTaskQueries.ts
│ │ │ │ ├── useVSCodeMessages.ts
│ │ │ │ └── useWebviewHeight.ts
│ │ │ ├── index.css
│ │ │ ├── index.tsx
│ │ │ ├── providers
│ │ │ │ └── QueryProvider.tsx
│ │ │ ├── reducers
│ │ │ │ └── appReducer.ts
│ │ │ ├── sidebar.tsx
│ │ │ ├── types
│ │ │ │ └── index.ts
│ │ │ └── utils
│ │ │ ├── logger.ts
│ │ │ └── toast.ts
│ │ └── tsconfig.json
│ └── mcp
│ ├── CHANGELOG.md
│ ├── package.json
│ ├── src
│ │ ├── index.ts
│ │ ├── shared
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ └── tools
│ │ ├── autopilot
│ │ │ ├── abort.tool.ts
│ │ │ ├── commit.tool.ts
│ │ │ ├── complete.tool.ts
│ │ │ ├── finalize.tool.ts
│ │ │ ├── index.ts
│ │ │ ├── next.tool.ts
│ │ │ ├── resume.tool.ts
│ │ │ ├── start.tool.ts
│ │ │ └── status.tool.ts
│ │ ├── README-ZOD-V3.md
│ │ └── tasks
│ │ ├── get-task.tool.ts
│ │ ├── get-tasks.tool.ts
│ │ └── index.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── assets
│ ├── .windsurfrules
│ ├── AGENTS.md
│ ├── claude
│ │ └── TM_COMMANDS_GUIDE.md
│ ├── config.json
│ ├── env.example
│ ├── example_prd_rpg.txt
│ ├── example_prd.txt
│ ├── GEMINI.md
│ ├── gitignore
│ ├── kiro-hooks
│ │ ├── tm-code-change-task-tracker.kiro.hook
│ │ ├── tm-complexity-analyzer.kiro.hook
│ │ ├── tm-daily-standup-assistant.kiro.hook
│ │ ├── tm-git-commit-task-linker.kiro.hook
│ │ ├── tm-pr-readiness-checker.kiro.hook
│ │ ├── tm-task-dependency-auto-progression.kiro.hook
│ │ └── tm-test-success-task-completer.kiro.hook
│ ├── roocode
│ │ ├── .roo
│ │ │ ├── rules-architect
│ │ │ │ └── architect-rules
│ │ │ ├── rules-ask
│ │ │ │ └── ask-rules
│ │ │ ├── rules-code
│ │ │ │ └── code-rules
│ │ │ ├── rules-debug
│ │ │ │ └── debug-rules
│ │ │ ├── rules-orchestrator
│ │ │ │ └── orchestrator-rules
│ │ │ └── rules-test
│ │ │ └── test-rules
│ │ └── .roomodes
│ ├── rules
│ │ ├── cursor_rules.mdc
│ │ ├── dev_workflow.mdc
│ │ ├── self_improve.mdc
│ │ ├── taskmaster_hooks_workflow.mdc
│ │ └── taskmaster.mdc
│ └── scripts_README.md
├── bin
│ └── task-master.js
├── biome.json
├── CHANGELOG.md
├── CLAUDE_CODE_PLUGIN.md
├── CLAUDE.md
├── context
│ ├── chats
│ │ ├── add-task-dependencies-1.md
│ │ └── max-min-tokens.txt.md
│ ├── fastmcp-core.txt
│ ├── fastmcp-docs.txt
│ ├── MCP_INTEGRATION.md
│ ├── mcp-js-sdk-docs.txt
│ ├── mcp-protocol-repo.txt
│ ├── mcp-protocol-schema-03262025.json
│ └── mcp-protocol-spec.txt
├── CONTRIBUTING.md
├── docs
│ ├── claude-code-integration.md
│ ├── CLI-COMMANDER-PATTERN.md
│ ├── command-reference.md
│ ├── configuration.md
│ ├── contributor-docs
│ │ ├── testing-roo-integration.md
│ │ └── worktree-setup.md
│ ├── cross-tag-task-movement.md
│ ├── examples
│ │ ├── claude-code-usage.md
│ │ └── codex-cli-usage.md
│ ├── examples.md
│ ├── licensing.md
│ ├── mcp-provider-guide.md
│ ├── mcp-provider.md
│ ├── migration-guide.md
│ ├── models.md
│ ├── providers
│ │ ├── codex-cli.md
│ │ └── gemini-cli.md
│ ├── README.md
│ ├── scripts
│ │ └── models-json-to-markdown.js
│ ├── task-structure.md
│ └── tutorial.md
├── images
│ ├── hamster-hiring.png
│ └── logo.png
├── index.js
├── jest.config.js
├── jest.resolver.cjs
├── LICENSE
├── llms-install.md
├── mcp-server
│ ├── server.js
│ └── src
│ ├── core
│ │ ├── __tests__
│ │ │ └── context-manager.test.js
│ │ ├── context-manager.js
│ │ ├── direct-functions
│ │ │ ├── add-dependency.js
│ │ │ ├── add-subtask.js
│ │ │ ├── add-tag.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── cache-stats.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── complexity-report.js
│ │ │ ├── copy-tag.js
│ │ │ ├── create-tag-from-branch.js
│ │ │ ├── delete-tag.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── fix-dependencies.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── initialize-project.js
│ │ │ ├── list-tags.js
│ │ │ ├── models.js
│ │ │ ├── move-task-cross-tag.js
│ │ │ ├── move-task.js
│ │ │ ├── next-task.js
│ │ │ ├── parse-prd.js
│ │ │ ├── remove-dependency.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── rename-tag.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── rules.js
│ │ │ ├── scope-down.js
│ │ │ ├── scope-up.js
│ │ │ ├── set-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ ├── update-tasks.js
│ │ │ ├── use-tag.js
│ │ │ └── validate-dependencies.js
│ │ ├── task-master-core.js
│ │ └── utils
│ │ ├── env-utils.js
│ │ └── path-utils.js
│ ├── custom-sdk
│ │ ├── errors.js
│ │ ├── index.js
│ │ ├── json-extractor.js
│ │ ├── language-model.js
│ │ ├── message-converter.js
│ │ └── schema-converter.js
│ ├── index.js
│ ├── logger.js
│ ├── providers
│ │ └── mcp-provider.js
│ └── tools
│ ├── add-dependency.js
│ ├── add-subtask.js
│ ├── add-tag.js
│ ├── add-task.js
│ ├── analyze.js
│ ├── clear-subtasks.js
│ ├── complexity-report.js
│ ├── copy-tag.js
│ ├── delete-tag.js
│ ├── expand-all.js
│ ├── expand-task.js
│ ├── fix-dependencies.js
│ ├── generate.js
│ ├── get-operation-status.js
│ ├── index.js
│ ├── initialize-project.js
│ ├── list-tags.js
│ ├── models.js
│ ├── move-task.js
│ ├── next-task.js
│ ├── parse-prd.js
│ ├── README-ZOD-V3.md
│ ├── remove-dependency.js
│ ├── remove-subtask.js
│ ├── remove-task.js
│ ├── rename-tag.js
│ ├── research.js
│ ├── response-language.js
│ ├── rules.js
│ ├── scope-down.js
│ ├── scope-up.js
│ ├── set-task-status.js
│ ├── tool-registry.js
│ ├── update-subtask.js
│ ├── update-task.js
│ ├── update.js
│ ├── use-tag.js
│ ├── utils.js
│ └── validate-dependencies.js
├── mcp-test.js
├── output.json
├── package-lock.json
├── package.json
├── packages
│ ├── ai-sdk-provider-grok-cli
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── errors.test.ts
│ │ │ ├── errors.ts
│ │ │ ├── grok-cli-language-model.ts
│ │ │ ├── grok-cli-provider.test.ts
│ │ │ ├── grok-cli-provider.ts
│ │ │ ├── index.ts
│ │ │ ├── json-extractor.test.ts
│ │ │ ├── json-extractor.ts
│ │ │ ├── message-converter.test.ts
│ │ │ ├── message-converter.ts
│ │ │ └── types.ts
│ │ └── tsconfig.json
│ ├── build-config
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── src
│ │ │ └── tsdown.base.ts
│ │ └── tsconfig.json
│ ├── claude-code-plugin
│ │ ├── .claude-plugin
│ │ │ └── plugin.json
│ │ ├── .gitignore
│ │ ├── agents
│ │ │ ├── task-checker.md
│ │ │ ├── task-executor.md
│ │ │ └── task-orchestrator.md
│ │ ├── CHANGELOG.md
│ │ ├── commands
│ │ │ ├── add-dependency.md
│ │ │ ├── add-subtask.md
│ │ │ ├── add-task.md
│ │ │ ├── analyze-complexity.md
│ │ │ ├── analyze-project.md
│ │ │ ├── auto-implement-tasks.md
│ │ │ ├── command-pipeline.md
│ │ │ ├── complexity-report.md
│ │ │ ├── convert-task-to-subtask.md
│ │ │ ├── expand-all-tasks.md
│ │ │ ├── expand-task.md
│ │ │ ├── fix-dependencies.md
│ │ │ ├── generate-tasks.md
│ │ │ ├── help.md
│ │ │ ├── init-project-quick.md
│ │ │ ├── init-project.md
│ │ │ ├── install-taskmaster.md
│ │ │ ├── learn.md
│ │ │ ├── list-tasks-by-status.md
│ │ │ ├── list-tasks-with-subtasks.md
│ │ │ ├── list-tasks.md
│ │ │ ├── next-task.md
│ │ │ ├── parse-prd-with-research.md
│ │ │ ├── parse-prd.md
│ │ │ ├── project-status.md
│ │ │ ├── quick-install-taskmaster.md
│ │ │ ├── remove-all-subtasks.md
│ │ │ ├── remove-dependency.md
│ │ │ ├── remove-subtask.md
│ │ │ ├── remove-subtasks.md
│ │ │ ├── remove-task.md
│ │ │ ├── setup-models.md
│ │ │ ├── show-task.md
│ │ │ ├── smart-workflow.md
│ │ │ ├── sync-readme.md
│ │ │ ├── tm-main.md
│ │ │ ├── to-cancelled.md
│ │ │ ├── to-deferred.md
│ │ │ ├── to-done.md
│ │ │ ├── to-in-progress.md
│ │ │ ├── to-pending.md
│ │ │ ├── to-review.md
│ │ │ ├── update-single-task.md
│ │ │ ├── update-task.md
│ │ │ ├── update-tasks-from-id.md
│ │ │ ├── validate-dependencies.md
│ │ │ └── view-models.md
│ │ ├── mcp.json
│ │ └── package.json
│ ├── tm-bridge
│ │ ├── CHANGELOG.md
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── src
│ │ │ ├── add-tag-bridge.ts
│ │ │ ├── bridge-types.ts
│ │ │ ├── bridge-utils.ts
│ │ │ ├── expand-bridge.ts
│ │ │ ├── index.ts
│ │ │ ├── tags-bridge.ts
│ │ │ ├── update-bridge.ts
│ │ │ └── use-tag-bridge.ts
│ │ └── tsconfig.json
│ └── tm-core
│ ├── .gitignore
│ ├── CHANGELOG.md
│ ├── docs
│ │ └── listTasks-architecture.md
│ ├── package.json
│ ├── POC-STATUS.md
│ ├── README.md
│ ├── src
│ │ ├── common
│ │ │ ├── constants
│ │ │ │ ├── index.ts
│ │ │ │ ├── paths.ts
│ │ │ │ └── providers.ts
│ │ │ ├── errors
│ │ │ │ ├── index.ts
│ │ │ │ └── task-master-error.ts
│ │ │ ├── interfaces
│ │ │ │ ├── configuration.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── storage.interface.ts
│ │ │ ├── logger
│ │ │ │ ├── factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── logger.spec.ts
│ │ │ │ └── logger.ts
│ │ │ ├── mappers
│ │ │ │ ├── TaskMapper.test.ts
│ │ │ │ └── TaskMapper.ts
│ │ │ ├── types
│ │ │ │ ├── database.types.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── legacy.ts
│ │ │ │ └── repository-types.ts
│ │ │ └── utils
│ │ │ ├── git-utils.ts
│ │ │ ├── id-generator.ts
│ │ │ ├── index.ts
│ │ │ ├── path-helpers.ts
│ │ │ ├── path-normalizer.spec.ts
│ │ │ ├── path-normalizer.ts
│ │ │ ├── project-root-finder.spec.ts
│ │ │ ├── project-root-finder.ts
│ │ │ ├── run-id-generator.spec.ts
│ │ │ └── run-id-generator.ts
│ │ ├── index.ts
│ │ ├── modules
│ │ │ ├── ai
│ │ │ │ ├── index.ts
│ │ │ │ ├── interfaces
│ │ │ │ │ └── ai-provider.interface.ts
│ │ │ │ └── providers
│ │ │ │ ├── base-provider.ts
│ │ │ │ └── index.ts
│ │ │ ├── auth
│ │ │ │ ├── auth-domain.spec.ts
│ │ │ │ ├── auth-domain.ts
│ │ │ │ ├── config.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── auth-manager.spec.ts
│ │ │ │ │ └── auth-manager.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── context-store.ts
│ │ │ │ │ ├── oauth-service.ts
│ │ │ │ │ ├── organization.service.ts
│ │ │ │ │ ├── supabase-session-storage.spec.ts
│ │ │ │ │ └── supabase-session-storage.ts
│ │ │ │ └── types.ts
│ │ │ ├── briefs
│ │ │ │ ├── briefs-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── brief-service.ts
│ │ │ │ ├── types.ts
│ │ │ │ └── utils
│ │ │ │ └── url-parser.ts
│ │ │ ├── commands
│ │ │ │ └── index.ts
│ │ │ ├── config
│ │ │ │ ├── config-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ ├── config-manager.spec.ts
│ │ │ │ │ └── config-manager.ts
│ │ │ │ └── services
│ │ │ │ ├── config-loader.service.spec.ts
│ │ │ │ ├── config-loader.service.ts
│ │ │ │ ├── config-merger.service.spec.ts
│ │ │ │ ├── config-merger.service.ts
│ │ │ │ ├── config-persistence.service.spec.ts
│ │ │ │ ├── config-persistence.service.ts
│ │ │ │ ├── environment-config-provider.service.spec.ts
│ │ │ │ ├── environment-config-provider.service.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── runtime-state-manager.service.spec.ts
│ │ │ │ └── runtime-state-manager.service.ts
│ │ │ ├── dependencies
│ │ │ │ └── index.ts
│ │ │ ├── execution
│ │ │ │ ├── executors
│ │ │ │ │ ├── base-executor.ts
│ │ │ │ │ ├── claude-executor.ts
│ │ │ │ │ └── executor-factory.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── executor-service.ts
│ │ │ │ └── types.ts
│ │ │ ├── git
│ │ │ │ ├── adapters
│ │ │ │ │ ├── git-adapter.test.ts
│ │ │ │ │ └── git-adapter.ts
│ │ │ │ ├── git-domain.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── services
│ │ │ │ ├── branch-name-generator.spec.ts
│ │ │ │ ├── branch-name-generator.ts
│ │ │ │ ├── commit-message-generator.test.ts
│ │ │ │ ├── commit-message-generator.ts
│ │ │ │ ├── scope-detector.test.ts
│ │ │ │ ├── scope-detector.ts
│ │ │ │ ├── template-engine.test.ts
│ │ │ │ └── template-engine.ts
│ │ │ ├── integration
│ │ │ │ ├── clients
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── supabase-client.ts
│ │ │ │ ├── integration-domain.ts
│ │ │ │ └── services
│ │ │ │ ├── export.service.ts
│ │ │ │ ├── task-expansion.service.ts
│ │ │ │ └── task-retrieval.service.ts
│ │ │ ├── reports
│ │ │ │ ├── index.ts
│ │ │ │ ├── managers
│ │ │ │ │ └── complexity-report-manager.ts
│ │ │ │ └── types.ts
│ │ │ ├── storage
│ │ │ │ ├── adapters
│ │ │ │ │ ├── activity-logger.ts
│ │ │ │ │ ├── api-storage.ts
│ │ │ │ │ └── file-storage
│ │ │ │ │ ├── file-operations.ts
│ │ │ │ │ ├── file-storage.ts
│ │ │ │ │ ├── format-handler.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── path-resolver.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── services
│ │ │ │ │ └── storage-factory.ts
│ │ │ │ └── utils
│ │ │ │ └── api-client.ts
│ │ │ ├── tasks
│ │ │ │ ├── entities
│ │ │ │ │ └── task.entity.ts
│ │ │ │ ├── parser
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories
│ │ │ │ │ ├── supabase
│ │ │ │ │ │ ├── dependency-fetcher.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── supabase-repository.ts
│ │ │ │ │ └── task-repository.interface.ts
│ │ │ │ ├── services
│ │ │ │ │ ├── preflight-checker.service.ts
│ │ │ │ │ ├── tag.service.ts
│ │ │ │ │ ├── task-execution-service.ts
│ │ │ │ │ ├── task-loader.service.ts
│ │ │ │ │ └── task-service.ts
│ │ │ │ └── tasks-domain.ts
│ │ │ ├── ui
│ │ │ │ └── index.ts
│ │ │ └── workflow
│ │ │ ├── managers
│ │ │ │ ├── workflow-state-manager.spec.ts
│ │ │ │ └── workflow-state-manager.ts
│ │ │ ├── orchestrators
│ │ │ │ ├── workflow-orchestrator.test.ts
│ │ │ │ └── workflow-orchestrator.ts
│ │ │ ├── services
│ │ │ │ ├── test-result-validator.test.ts
│ │ │ │ ├── test-result-validator.ts
│ │ │ │ ├── test-result-validator.types.ts
│ │ │ │ ├── workflow-activity-logger.ts
│ │ │ │ └── workflow.service.ts
│ │ │ ├── types.ts
│ │ │ └── workflow-domain.ts
│ │ ├── subpath-exports.test.ts
│ │ ├── tm-core.ts
│ │ └── utils
│ │ └── time.utils.ts
│ ├── tests
│ │ ├── auth
│ │ │ └── auth-refresh.test.ts
│ │ ├── integration
│ │ │ ├── auth-token-refresh.test.ts
│ │ │ ├── list-tasks.test.ts
│ │ │ └── storage
│ │ │ └── activity-logger.test.ts
│ │ ├── mocks
│ │ │ └── mock-provider.ts
│ │ ├── setup.ts
│ │ └── unit
│ │ ├── base-provider.test.ts
│ │ ├── executor.test.ts
│ │ └── smoke.test.ts
│ ├── tsconfig.json
│ └── vitest.config.ts
├── README-task-master.md
├── README.md
├── scripts
│ ├── create-worktree.sh
│ ├── dev.js
│ ├── init.js
│ ├── list-worktrees.sh
│ ├── modules
│ │ ├── ai-services-unified.js
│ │ ├── bridge-utils.js
│ │ ├── commands.js
│ │ ├── config-manager.js
│ │ ├── dependency-manager.js
│ │ ├── index.js
│ │ ├── prompt-manager.js
│ │ ├── supported-models.json
│ │ ├── sync-readme.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.js
│ │ │ ├── add-task.js
│ │ │ ├── analyze-task-complexity.js
│ │ │ ├── clear-subtasks.js
│ │ │ ├── expand-all-tasks.js
│ │ │ ├── expand-task.js
│ │ │ ├── find-next-task.js
│ │ │ ├── generate-task-files.js
│ │ │ ├── is-task-dependent.js
│ │ │ ├── list-tasks.js
│ │ │ ├── migrate.js
│ │ │ ├── models.js
│ │ │ ├── move-task.js
│ │ │ ├── parse-prd
│ │ │ │ ├── index.js
│ │ │ │ ├── parse-prd-config.js
│ │ │ │ ├── parse-prd-helpers.js
│ │ │ │ ├── parse-prd-non-streaming.js
│ │ │ │ ├── parse-prd-streaming.js
│ │ │ │ └── parse-prd.js
│ │ │ ├── remove-subtask.js
│ │ │ ├── remove-task.js
│ │ │ ├── research.js
│ │ │ ├── response-language.js
│ │ │ ├── scope-adjustment.js
│ │ │ ├── set-task-status.js
│ │ │ ├── tag-management.js
│ │ │ ├── task-exists.js
│ │ │ ├── update-single-task-status.js
│ │ │ ├── update-subtask-by-id.js
│ │ │ ├── update-task-by-id.js
│ │ │ └── update-tasks.js
│ │ ├── task-manager.js
│ │ ├── ui.js
│ │ ├── update-config-tokens.js
│ │ ├── utils
│ │ │ ├── contextGatherer.js
│ │ │ ├── fuzzyTaskSearch.js
│ │ │ └── git-utils.js
│ │ └── utils.js
│ ├── task-complexity-report.json
│ ├── test-claude-errors.js
│ └── test-claude.js
├── sonar-project.properties
├── src
│ ├── ai-providers
│ │ ├── anthropic.js
│ │ ├── azure.js
│ │ ├── base-provider.js
│ │ ├── bedrock.js
│ │ ├── claude-code.js
│ │ ├── codex-cli.js
│ │ ├── gemini-cli.js
│ │ ├── google-vertex.js
│ │ ├── google.js
│ │ ├── grok-cli.js
│ │ ├── groq.js
│ │ ├── index.js
│ │ ├── lmstudio.js
│ │ ├── ollama.js
│ │ ├── openai-compatible.js
│ │ ├── openai.js
│ │ ├── openrouter.js
│ │ ├── perplexity.js
│ │ ├── xai.js
│ │ ├── zai-coding.js
│ │ └── zai.js
│ ├── constants
│ │ ├── commands.js
│ │ ├── paths.js
│ │ ├── profiles.js
│ │ ├── rules-actions.js
│ │ ├── task-priority.js
│ │ └── task-status.js
│ ├── profiles
│ │ ├── amp.js
│ │ ├── base-profile.js
│ │ ├── claude.js
│ │ ├── cline.js
│ │ ├── codex.js
│ │ ├── cursor.js
│ │ ├── gemini.js
│ │ ├── index.js
│ │ ├── kilo.js
│ │ ├── kiro.js
│ │ ├── opencode.js
│ │ ├── roo.js
│ │ ├── trae.js
│ │ ├── vscode.js
│ │ ├── windsurf.js
│ │ └── zed.js
│ ├── progress
│ │ ├── base-progress-tracker.js
│ │ ├── cli-progress-factory.js
│ │ ├── parse-prd-tracker.js
│ │ ├── progress-tracker-builder.js
│ │ └── tracker-ui.js
│ ├── prompts
│ │ ├── add-task.json
│ │ ├── analyze-complexity.json
│ │ ├── expand-task.json
│ │ ├── parse-prd.json
│ │ ├── README.md
│ │ ├── research.json
│ │ ├── schemas
│ │ │ ├── parameter.schema.json
│ │ │ ├── prompt-template.schema.json
│ │ │ ├── README.md
│ │ │ └── variant.schema.json
│ │ ├── update-subtask.json
│ │ ├── update-task.json
│ │ └── update-tasks.json
│ ├── provider-registry
│ │ └── index.js
│ ├── schemas
│ │ ├── add-task.js
│ │ ├── analyze-complexity.js
│ │ ├── base-schemas.js
│ │ ├── expand-task.js
│ │ ├── parse-prd.js
│ │ ├── registry.js
│ │ ├── update-subtask.js
│ │ ├── update-task.js
│ │ └── update-tasks.js
│ ├── task-master.js
│ ├── ui
│ │ ├── confirm.js
│ │ ├── indicators.js
│ │ └── parse-prd.js
│ └── utils
│ ├── asset-resolver.js
│ ├── create-mcp-config.js
│ ├── format.js
│ ├── getVersion.js
│ ├── logger-utils.js
│ ├── manage-gitignore.js
│ ├── path-utils.js
│ ├── profiles.js
│ ├── rule-transformer.js
│ ├── stream-parser.js
│ └── timeout-manager.js
├── test-clean-tags.js
├── test-config-manager.js
├── test-prd.txt
├── test-tag-functions.js
├── test-version-check-full.js
├── test-version-check.js
├── tests
│ ├── e2e
│ │ ├── e2e_helpers.sh
│ │ ├── parse_llm_output.cjs
│ │ ├── run_e2e.sh
│ │ ├── run_fallback_verification.sh
│ │ └── test_llm_analysis.sh
│ ├── fixtures
│ │ ├── .taskmasterconfig
│ │ ├── sample-claude-response.js
│ │ ├── sample-prd.txt
│ │ └── sample-tasks.js
│ ├── helpers
│ │ └── tool-counts.js
│ ├── integration
│ │ ├── claude-code-error-handling.test.js
│ │ ├── claude-code-optional.test.js
│ │ ├── cli
│ │ │ ├── commands.test.js
│ │ │ ├── complex-cross-tag-scenarios.test.js
│ │ │ └── move-cross-tag.test.js
│ │ ├── manage-gitignore.test.js
│ │ ├── mcp-server
│ │ │ └── direct-functions.test.js
│ │ ├── move-task-cross-tag.integration.test.js
│ │ ├── move-task-simple.integration.test.js
│ │ ├── profiles
│ │ │ ├── amp-init-functionality.test.js
│ │ │ ├── claude-init-functionality.test.js
│ │ │ ├── cline-init-functionality.test.js
│ │ │ ├── codex-init-functionality.test.js
│ │ │ ├── cursor-init-functionality.test.js
│ │ │ ├── gemini-init-functionality.test.js
│ │ │ ├── opencode-init-functionality.test.js
│ │ │ ├── roo-files-inclusion.test.js
│ │ │ ├── roo-init-functionality.test.js
│ │ │ ├── rules-files-inclusion.test.js
│ │ │ ├── trae-init-functionality.test.js
│ │ │ ├── vscode-init-functionality.test.js
│ │ │ └── windsurf-init-functionality.test.js
│ │ └── providers
│ │ └── temperature-support.test.js
│ ├── manual
│ │ ├── progress
│ │ │ ├── parse-prd-analysis.js
│ │ │ ├── test-parse-prd.js
│ │ │ └── TESTING_GUIDE.md
│ │ └── prompts
│ │ ├── prompt-test.js
│ │ └── README.md
│ ├── README.md
│ ├── setup.js
│ └── unit
│ ├── ai-providers
│ │ ├── base-provider.test.js
│ │ ├── claude-code.test.js
│ │ ├── codex-cli.test.js
│ │ ├── gemini-cli.test.js
│ │ ├── lmstudio.test.js
│ │ ├── mcp-components.test.js
│ │ ├── openai-compatible.test.js
│ │ ├── openai.test.js
│ │ ├── provider-registry.test.js
│ │ ├── zai-coding.test.js
│ │ ├── zai-provider.test.js
│ │ ├── zai-schema-introspection.test.js
│ │ └── zai.test.js
│ ├── ai-services-unified.test.js
│ ├── commands.test.js
│ ├── config-manager.test.js
│ ├── config-manager.test.mjs
│ ├── dependency-manager.test.js
│ ├── init.test.js
│ ├── initialize-project.test.js
│ ├── kebab-case-validation.test.js
│ ├── manage-gitignore.test.js
│ ├── mcp
│ │ └── tools
│ │ ├── __mocks__
│ │ │ └── move-task.js
│ │ ├── add-task.test.js
│ │ ├── analyze-complexity.test.js
│ │ ├── expand-all.test.js
│ │ ├── get-tasks.test.js
│ │ ├── initialize-project.test.js
│ │ ├── move-task-cross-tag-options.test.js
│ │ ├── move-task-cross-tag.test.js
│ │ ├── remove-task.test.js
│ │ └── tool-registration.test.js
│ ├── mcp-providers
│ │ ├── mcp-components.test.js
│ │ └── mcp-provider.test.js
│ ├── parse-prd.test.js
│ ├── profiles
│ │ ├── amp-integration.test.js
│ │ ├── claude-integration.test.js
│ │ ├── cline-integration.test.js
│ │ ├── codex-integration.test.js
│ │ ├── cursor-integration.test.js
│ │ ├── gemini-integration.test.js
│ │ ├── kilo-integration.test.js
│ │ ├── kiro-integration.test.js
│ │ ├── mcp-config-validation.test.js
│ │ ├── opencode-integration.test.js
│ │ ├── profile-safety-check.test.js
│ │ ├── roo-integration.test.js
│ │ ├── rule-transformer-cline.test.js
│ │ ├── rule-transformer-cursor.test.js
│ │ ├── rule-transformer-gemini.test.js
│ │ ├── rule-transformer-kilo.test.js
│ │ ├── rule-transformer-kiro.test.js
│ │ ├── rule-transformer-opencode.test.js
│ │ ├── rule-transformer-roo.test.js
│ │ ├── rule-transformer-trae.test.js
│ │ ├── rule-transformer-vscode.test.js
│ │ ├── rule-transformer-windsurf.test.js
│ │ ├── rule-transformer-zed.test.js
│ │ ├── rule-transformer.test.js
│ │ ├── selective-profile-removal.test.js
│ │ ├── subdirectory-support.test.js
│ │ ├── trae-integration.test.js
│ │ ├── vscode-integration.test.js
│ │ ├── windsurf-integration.test.js
│ │ └── zed-integration.test.js
│ ├── progress
│ │ └── base-progress-tracker.test.js
│ ├── prompt-manager.test.js
│ ├── prompts
│ │ ├── expand-task-prompt.test.js
│ │ └── prompt-migration.test.js
│ ├── scripts
│ │ └── modules
│ │ ├── commands
│ │ │ ├── move-cross-tag.test.js
│ │ │ └── README.md
│ │ ├── dependency-manager
│ │ │ ├── circular-dependencies.test.js
│ │ │ ├── cross-tag-dependencies.test.js
│ │ │ └── fix-dependencies-command.test.js
│ │ ├── task-manager
│ │ │ ├── add-subtask.test.js
│ │ │ ├── add-task.test.js
│ │ │ ├── analyze-task-complexity.test.js
│ │ │ ├── clear-subtasks.test.js
│ │ │ ├── complexity-report-tag-isolation.test.js
│ │ │ ├── expand-all-tasks.test.js
│ │ │ ├── expand-task.test.js
│ │ │ ├── find-next-task.test.js
│ │ │ ├── generate-task-files.test.js
│ │ │ ├── list-tasks.test.js
│ │ │ ├── models-baseurl.test.js
│ │ │ ├── move-task-cross-tag.test.js
│ │ │ ├── move-task.test.js
│ │ │ ├── parse-prd-schema.test.js
│ │ │ ├── parse-prd.test.js
│ │ │ ├── remove-subtask.test.js
│ │ │ ├── remove-task.test.js
│ │ │ ├── research.test.js
│ │ │ ├── scope-adjustment.test.js
│ │ │ ├── set-task-status.test.js
│ │ │ ├── setup.js
│ │ │ ├── update-single-task-status.test.js
│ │ │ ├── update-subtask-by-id.test.js
│ │ │ ├── update-task-by-id.test.js
│ │ │ └── update-tasks.test.js
│ │ ├── ui
│ │ │ └── cross-tag-error-display.test.js
│ │ └── utils-tag-aware-paths.test.js
│ ├── task-finder.test.js
│ ├── task-manager
│ │ ├── clear-subtasks.test.js
│ │ ├── move-task.test.js
│ │ ├── tag-boundary.test.js
│ │ └── tag-management.test.js
│ ├── task-master.test.js
│ ├── ui
│ │ └── indicators.test.js
│ ├── ui.test.js
│ ├── utils-strip-ansi.test.js
│ └── utils.test.js
├── tsconfig.json
├── tsdown.config.ts
├── turbo.json
└── update-task-migration-plan.md
```
# Files
--------------------------------------------------------------------------------
/packages/tm-core/src/common/interfaces/configuration.interface.ts:
--------------------------------------------------------------------------------
```typescript
1 | /**
2 | * @fileoverview Configuration interface definitions for the tm-core package
3 | * This file defines the contract for configuration management
4 | */
5 |
6 | import type {
7 | StorageType,
8 | TaskComplexity,
9 | TaskPriority
10 | } from '../types/index.js';
11 |
12 | /**
13 | * Conventional Commit types allowed in workflow
14 | */
15 | export type CommitType =
16 | | 'feat'
17 | | 'fix'
18 | | 'refactor'
19 | | 'test'
20 | | 'docs'
21 | | 'chore';
22 |
23 | /**
24 | * Model configuration for different AI roles
25 | */
26 | export interface ModelConfig {
27 | /** Primary model for task generation and updates */
28 | main: string;
29 | /** Research model for enhanced task analysis (optional) */
30 | research?: string;
31 | /** Fallback model when primary fails */
32 | fallback: string;
33 | }
34 |
35 | /**
36 | * AI provider configuration
37 | */
38 | export interface ProviderConfig {
39 | /** Provider name (e.g., 'anthropic', 'openai', 'perplexity') */
40 | name: string;
41 | /** API key for the provider */
42 | apiKey?: string;
43 | /** Base URL override */
44 | baseUrl?: string;
45 | /** Custom configuration options */
46 | options?: Record<string, unknown>;
47 | /** Whether this provider is enabled */
48 | enabled?: boolean;
49 | }
50 |
51 | /**
52 | * Task generation and management settings
53 | */
54 | export interface TaskSettings {
55 | /** Default priority for new tasks */
56 | defaultPriority: TaskPriority;
57 | /** Default complexity for analysis */
58 | defaultComplexity: TaskComplexity;
59 | /**
60 | * Maximum number of subtasks per task
61 | * @minimum 1
62 | */
63 | maxSubtasks: number;
64 | /**
65 | * Maximum number of concurrent tasks
66 | * @minimum 1
67 | */
68 | maxConcurrentTasks: number;
69 | /** Enable automatic task ID generation */
70 | autoGenerateIds: boolean;
71 | /** Task ID prefix (e.g., 'TASK-', 'TM-') */
72 | taskIdPrefix?: string;
73 | /** Enable task dependency validation */
74 | validateDependencies: boolean;
75 | /** Enable automatic timestamps */
76 | enableTimestamps: boolean;
77 | /** Enable effort tracking */
78 | enableEffortTracking: boolean;
79 | }
80 |
81 | /**
82 | * Tag and context management settings
83 | */
84 | export interface TagSettings {
85 | /** Enable tag-based task organization */
86 | enableTags: boolean;
87 | /** Default tag for new tasks */
88 | defaultTag: string;
89 | /**
90 | * Maximum number of tags per task
91 | * @minimum 1
92 | */
93 | maxTagsPerTask: number;
94 | /** Enable automatic tag creation from Git branches */
95 | autoCreateFromBranch: boolean;
96 | /** Tag naming convention (kebab-case, camelCase, snake_case) */
97 | tagNamingConvention: 'kebab-case' | 'camelCase' | 'snake_case';
98 | }
99 |
100 | /**
101 | * Runtime storage configuration used for storage backend selection
102 | * This is what getStorageConfig() returns and what StorageFactory expects
103 | */
104 | export interface RuntimeStorageConfig {
105 | /** Storage backend type */
106 | type: StorageType;
107 | /** Base path for file storage (if configured) */
108 | basePath?: string;
109 | /** API endpoint for API storage (Hamster integration) */
110 | apiEndpoint?: string;
111 | /** Access token for API authentication */
112 | apiAccessToken?: string;
113 | /**
114 | * Indicates whether API is configured (has endpoint or token)
115 | * @computed Derived automatically from presence of apiEndpoint or apiAccessToken
116 | * @internal Should not be set manually - computed by ConfigManager
117 | */
118 | readonly apiConfigured: boolean;
119 | }
120 |
121 | /**
122 | * Storage and persistence settings
123 | * Extended storage settings including file operation preferences
124 | */
125 | export interface StorageSettings
126 | extends Omit<RuntimeStorageConfig, 'apiConfigured'> {
127 | /** Base path for file storage */
128 | basePath?: string;
129 | /**
130 | * Indicates whether API is configured
131 | * @computed Derived automatically from presence of apiEndpoint or apiAccessToken
132 | * @internal Should not be set manually in user config - computed by ConfigManager
133 | */
134 | readonly apiConfigured?: boolean;
135 | /** Enable automatic backups */
136 | enableBackup: boolean;
137 | /**
138 | * Maximum number of backups to retain
139 | * @minimum 0
140 | */
141 | maxBackups: number;
142 | /** Enable compression for storage */
143 | enableCompression: boolean;
144 | /** File encoding for text files */
145 | encoding: BufferEncoding;
146 | /** Enable atomic file operations */
147 | atomicOperations: boolean;
148 | }
149 |
150 | /**
151 | * Retry and resilience settings
152 | */
153 | export interface RetrySettings {
154 | /**
155 | * Number of retry attempts for failed operations
156 | * @minimum 0
157 | */
158 | retryAttempts: number;
159 | /**
160 | * Base delay between retries in milliseconds
161 | * @minimum 0
162 | */
163 | retryDelay: number;
164 | /**
165 | * Maximum delay between retries in milliseconds
166 | * @minimum 0
167 | */
168 | maxRetryDelay: number;
169 | /**
170 | * Exponential backoff multiplier
171 | * @minimum 1
172 | */
173 | backoffMultiplier: number;
174 | /**
175 | * Request timeout in milliseconds
176 | * @minimum 0
177 | */
178 | requestTimeout: number;
179 | /** Enable retry for network errors */
180 | retryOnNetworkError: boolean;
181 | /** Enable retry for rate limit errors */
182 | retryOnRateLimit: boolean;
183 | }
184 |
185 | /**
186 | * Logging and debugging settings
187 | */
188 | export interface LoggingSettings {
189 | /** Enable logging */
190 | enabled: boolean;
191 | /** Log level (error, warn, info, debug) */
192 | level: 'error' | 'warn' | 'info' | 'debug';
193 | /** Log file path (optional) */
194 | filePath?: string;
195 | /** Enable request/response logging */
196 | logRequests: boolean;
197 | /** Enable performance metrics logging */
198 | logPerformance: boolean;
199 | /** Enable error stack traces */
200 | logStackTraces: boolean;
201 | /**
202 | * Maximum log file size in MB
203 | * @minimum 1
204 | */
205 | maxFileSize: number;
206 | /**
207 | * Maximum number of log files to retain
208 | * @minimum 1
209 | */
210 | maxFiles: number;
211 | }
212 |
213 | /**
214 | * Security and validation settings
215 | */
216 | export interface SecuritySettings {
217 | /** Enable API key validation */
218 | validateApiKeys: boolean;
219 | /** Enable request rate limiting */
220 | enableRateLimit: boolean;
221 | /**
222 | * Maximum requests per minute
223 | * @minimum 1
224 | */
225 | maxRequestsPerMinute: number;
226 | /** Enable input sanitization */
227 | sanitizeInputs: boolean;
228 | /**
229 | * Maximum prompt length in characters
230 | * @minimum 1
231 | */
232 | maxPromptLength: number;
233 | /** Allowed file extensions for imports */
234 | allowedFileExtensions: string[];
235 | /** Enable CORS protection */
236 | enableCors: boolean;
237 | }
238 |
239 | /**
240 | * Workflow and autopilot TDD settings
241 | */
242 | export interface WorkflowSettings {
243 | /** Enable autopilot/TDD workflow features */
244 | enableAutopilot: boolean;
245 | /**
246 | * Maximum retry attempts for phase validation
247 | * @minimum 1
248 | * @maximum 10
249 | */
250 | maxPhaseAttempts: number;
251 | /** Branch naming pattern for workflow branches */
252 | branchPattern: string;
253 | /** Require clean working tree before starting workflow */
254 | requireCleanWorkingTree: boolean;
255 | /** Automatically stage all changes during commit phase */
256 | autoStageChanges: boolean;
257 | /** Include co-author attribution in commits */
258 | includeCoAuthor: boolean;
259 | /** Co-author name for commit messages */
260 | coAuthorName: string;
261 | /** Co-author email for commit messages (defaults to [email protected]) */
262 | coAuthorEmail: string;
263 | /** Test result thresholds for phase validation */
264 | testThresholds: {
265 | /**
266 | * Minimum test count for valid RED phase
267 | * @minimum 0
268 | */
269 | minTests: number;
270 | /**
271 | * Maximum allowed failing tests in GREEN phase
272 | * @minimum 0
273 | */
274 | maxFailuresInGreen: number;
275 | };
276 | /** Commit message template pattern */
277 | commitMessageTemplate: string;
278 | /** Conventional commit types allowed */
279 | allowedCommitTypes: readonly CommitType[];
280 | /**
281 | * Default commit type for autopilot
282 | * @validation Must be present in allowedCommitTypes array
283 | */
284 | defaultCommitType: CommitType;
285 | /**
286 | * Timeout for workflow operations in milliseconds
287 | * @minimum 0
288 | */
289 | operationTimeout: number;
290 | /** Enable activity logging for workflow events */
291 | enableActivityLogging: boolean;
292 | /** Path to store workflow activity logs */
293 | activityLogPath: string;
294 | /** Enable automatic backup of workflow state */
295 | enableStateBackup: boolean;
296 | /**
297 | * Maximum workflow state backups to retain
298 | * @minimum 0
299 | */
300 | maxStateBackups: number;
301 | /** Abort workflow if validation fails after max attempts */
302 | abortOnMaxAttempts: boolean;
303 | }
304 |
305 | /**
306 | * Main configuration interface for Task Master core
307 | */
308 | export interface IConfiguration {
309 | /** Project root path */
310 | projectPath: string;
311 |
312 | /** Current AI provider name */
313 | aiProvider: string;
314 |
315 | /** API keys for different providers */
316 | apiKeys: Record<string, string>;
317 |
318 | /** Model configuration for different roles */
319 | models: ModelConfig;
320 |
321 | /** Provider configurations */
322 | providers: Record<string, ProviderConfig>;
323 |
324 | /** Task management settings */
325 | tasks: TaskSettings;
326 |
327 | /** Tag and context settings */
328 | tags: TagSettings;
329 |
330 | /** Workflow and autopilot settings */
331 | workflow: WorkflowSettings;
332 |
333 | /** Storage configuration */
334 | storage: StorageSettings;
335 |
336 | /** Retry and resilience settings */
337 | retry: RetrySettings;
338 |
339 | /** Logging configuration */
340 | logging: LoggingSettings;
341 |
342 | /** Security settings */
343 | security: SecuritySettings;
344 |
345 | /** Custom user-defined settings */
346 | custom?: Record<string, unknown>;
347 |
348 | /** Configuration version for migration purposes */
349 | version: string;
350 |
351 | /** Last updated timestamp */
352 | lastUpdated: string;
353 | }
354 |
355 | /**
356 | * Partial configuration for updates (all fields optional)
357 | */
358 | export type PartialConfiguration = Partial<IConfiguration>;
359 |
360 | /**
361 | * Configuration validation result
362 | */
363 | export interface ConfigValidationResult {
364 | /** Whether the configuration is valid */
365 | isValid: boolean;
366 | /** Array of error messages */
367 | errors: string[];
368 | /** Array of warning messages */
369 | warnings: string[];
370 | /** Suggested fixes */
371 | suggestions?: string[];
372 | }
373 |
374 | /**
375 | * Environment variable configuration mapping
376 | */
377 | export interface EnvironmentConfig {
378 | /** Mapping of environment variables to config paths */
379 | variables: Record<string, string>;
380 | /** Prefix for environment variables */
381 | prefix: string;
382 | /** Whether to override existing config with env vars */
383 | override: boolean;
384 | }
385 |
386 | /**
387 | * Configuration schema definition for validation
388 | */
389 | export interface ConfigSchema {
390 | /** Schema for the main configuration */
391 | properties: Record<string, ConfigProperty>;
392 | /** Required properties */
393 | required: string[];
394 | /** Additional properties allowed */
395 | additionalProperties: boolean;
396 | }
397 |
398 | /**
399 | * Configuration property schema
400 | */
401 | export interface ConfigProperty {
402 | /** Property type */
403 | type: 'string' | 'number' | 'boolean' | 'object' | 'array';
404 | /** Property description */
405 | description?: string;
406 | /** Default value */
407 | default?: unknown;
408 | /** Allowed values for enums */
409 | enum?: unknown[];
410 | /** Minimum value (for numbers) */
411 | minimum?: number;
412 | /** Maximum value (for numbers) */
413 | maximum?: number;
414 | /** Pattern for string validation */
415 | pattern?: string;
416 | /** Nested properties (for objects) */
417 | properties?: Record<string, ConfigProperty>;
418 | /** Array item type (for arrays) */
419 | items?: ConfigProperty;
420 | /** Whether the property is required */
421 | required?: boolean;
422 | }
423 |
424 | /**
425 | * Default configuration factory
426 | */
427 | export interface IConfigurationFactory {
428 | /**
429 | * Create a default configuration
430 | * @param projectPath - Project root path
431 | * @returns Default configuration object
432 | */
433 | createDefault(projectPath: string): IConfiguration;
434 |
435 | /**
436 | * Merge configurations with precedence
437 | * @param base - Base configuration
438 | * @param override - Override configuration
439 | * @returns Merged configuration
440 | */
441 | merge(base: IConfiguration, override: PartialConfiguration): IConfiguration;
442 |
443 | /**
444 | * Validate configuration against schema
445 | * @param config - Configuration to validate
446 | * @returns Validation result
447 | */
448 | validate(config: IConfiguration): ConfigValidationResult;
449 |
450 | /**
451 | * Load configuration from environment variables
452 | * @param envConfig - Environment variable mapping
453 | * @returns Partial configuration from environment
454 | */
455 | loadFromEnvironment(envConfig: EnvironmentConfig): PartialConfiguration;
456 |
457 | /**
458 | * Get configuration schema
459 | * @returns Configuration schema definition
460 | */
461 | getSchema(): ConfigSchema;
462 | }
463 |
464 | /**
465 | * Configuration manager interface
466 | */
467 | export interface IConfigurationManager {
468 | /**
469 | * Load configuration from file or create default
470 | * @param configPath - Path to configuration file
471 | * @returns Promise that resolves to configuration
472 | */
473 | load(configPath?: string): Promise<IConfiguration>;
474 |
475 | /**
476 | * Save configuration to file
477 | * @param config - Configuration to save
478 | * @param configPath - Optional path override
479 | * @returns Promise that resolves when save is complete
480 | */
481 | save(config: IConfiguration, configPath?: string): Promise<void>;
482 |
483 | /**
484 | * Update configuration with partial changes
485 | * @param updates - Partial configuration updates
486 | * @returns Promise that resolves to updated configuration
487 | */
488 | update(updates: PartialConfiguration): Promise<IConfiguration>;
489 |
490 | /**
491 | * Get current configuration
492 | * @returns Current configuration object
493 | */
494 | getConfig(): IConfiguration;
495 |
496 | /**
497 | * Watch for configuration changes
498 | * @param callback - Function to call when config changes
499 | * @returns Function to stop watching
500 | */
501 | watch(callback: (config: IConfiguration) => void): () => void;
502 |
503 | /**
504 | * Validate current configuration
505 | * @returns Validation result
506 | */
507 | validate(): ConfigValidationResult;
508 |
509 | /**
510 | * Reset configuration to defaults
511 | * @returns Promise that resolves when reset is complete
512 | */
513 | reset(): Promise<void>;
514 | }
515 |
516 | /**
517 | * Constants for default configuration values
518 | */
519 | export const DEFAULT_CONFIG_VALUES = {
520 | MODELS: {
521 | MAIN: 'claude-sonnet-4-20250514',
522 | FALLBACK: 'claude-3-7-sonnet-20250219'
523 | },
524 | TASKS: {
525 | DEFAULT_PRIORITY: 'medium' as TaskPriority,
526 | DEFAULT_COMPLEXITY: 'moderate' as TaskComplexity,
527 | MAX_SUBTASKS: 20,
528 | MAX_CONCURRENT: 5,
529 | TASK_ID_PREFIX: 'TASK-'
530 | },
531 | TAGS: {
532 | DEFAULT_TAG: 'master',
533 | MAX_TAGS_PER_TASK: 10,
534 | NAMING_CONVENTION: 'kebab-case' as const
535 | },
536 | WORKFLOW: {
537 | ENABLE_AUTOPILOT: true,
538 | MAX_PHASE_ATTEMPTS: 3,
539 | BRANCH_PATTERN: 'task-{taskId}',
540 | REQUIRE_CLEAN_WORKING_TREE: true,
541 | AUTO_STAGE_CHANGES: true,
542 | INCLUDE_CO_AUTHOR: true,
543 | CO_AUTHOR_NAME: 'TaskMaster AI',
544 | CO_AUTHOR_EMAIL: '[email protected]',
545 | MIN_TESTS: 1,
546 | MAX_FAILURES_IN_GREEN: 0,
547 | COMMIT_MESSAGE_TEMPLATE:
548 | '{type}({scope}): {description} (Task {taskId}.{subtaskIndex})',
549 | ALLOWED_COMMIT_TYPES: [
550 | 'feat',
551 | 'fix',
552 | 'refactor',
553 | 'test',
554 | 'docs',
555 | 'chore'
556 | ] as const satisfies readonly CommitType[],
557 | DEFAULT_COMMIT_TYPE: 'feat' as CommitType,
558 | OPERATION_TIMEOUT: 60000,
559 | ENABLE_ACTIVITY_LOGGING: true,
560 | ACTIVITY_LOG_PATH: '.taskmaster/logs/workflow-activity.log',
561 | ENABLE_STATE_BACKUP: true,
562 | MAX_STATE_BACKUPS: 5,
563 | ABORT_ON_MAX_ATTEMPTS: false
564 | },
565 | STORAGE: {
566 | TYPE: 'auto' as const,
567 | ENCODING: 'utf8' as BufferEncoding,
568 | MAX_BACKUPS: 5
569 | },
570 | RETRY: {
571 | ATTEMPTS: 3,
572 | DELAY: 1000,
573 | MAX_DELAY: 30000,
574 | BACKOFF_MULTIPLIER: 2,
575 | TIMEOUT: 30000
576 | },
577 | LOGGING: {
578 | LEVEL: 'info' as const,
579 | MAX_FILE_SIZE: 10,
580 | MAX_FILES: 5
581 | },
582 | SECURITY: {
583 | MAX_REQUESTS_PER_MINUTE: 60,
584 | MAX_PROMPT_LENGTH: 100000,
585 | ALLOWED_EXTENSIONS: ['.txt', '.md', '.json']
586 | },
587 | VERSION: '1.0.0'
588 | } as const;
589 |
```
--------------------------------------------------------------------------------
/scripts/modules/prompt-manager.js:
--------------------------------------------------------------------------------
```javascript
1 | import { log } from './utils.js';
2 | import Ajv from 'ajv';
3 | import addFormats from 'ajv-formats';
4 |
5 | // Import all prompt templates directly
6 | import analyzeComplexityPrompt from '../../src/prompts/analyze-complexity.json' with {
7 | type: 'json'
8 | };
9 | import expandTaskPrompt from '../../src/prompts/expand-task.json' with {
10 | type: 'json'
11 | };
12 | import addTaskPrompt from '../../src/prompts/add-task.json' with {
13 | type: 'json'
14 | };
15 | import researchPrompt from '../../src/prompts/research.json' with {
16 | type: 'json'
17 | };
18 | import parsePrdPrompt from '../../src/prompts/parse-prd.json' with {
19 | type: 'json'
20 | };
21 | import updateTaskPrompt from '../../src/prompts/update-task.json' with {
22 | type: 'json'
23 | };
24 | import updateTasksPrompt from '../../src/prompts/update-tasks.json' with {
25 | type: 'json'
26 | };
27 | import updateSubtaskPrompt from '../../src/prompts/update-subtask.json' with {
28 | type: 'json'
29 | };
30 |
31 | // Import schema for validation
32 | import promptTemplateSchema from '../../src/prompts/schemas/prompt-template.schema.json' with {
33 | type: 'json'
34 | };
35 |
36 | /**
37 | * Manages prompt templates for AI interactions
38 | */
39 | export class PromptManager {
40 | constructor() {
41 | // Store all prompts in a map for easy lookup
42 | this.prompts = new Map([
43 | ['analyze-complexity', analyzeComplexityPrompt],
44 | ['expand-task', expandTaskPrompt],
45 | ['add-task', addTaskPrompt],
46 | ['research', researchPrompt],
47 | ['parse-prd', parsePrdPrompt],
48 | ['update-task', updateTaskPrompt],
49 | ['update-tasks', updateTasksPrompt],
50 | ['update-subtask', updateSubtaskPrompt]
51 | ]);
52 |
53 | this.cache = new Map();
54 | this.setupValidation();
55 | }
56 |
57 | /**
58 | * Set up JSON schema validation
59 | * @private
60 | */
61 | setupValidation() {
62 | this.ajv = new Ajv({ allErrors: true, strict: false });
63 | addFormats(this.ajv);
64 |
65 | try {
66 | // Use the imported schema directly
67 | this.validatePrompt = this.ajv.compile(promptTemplateSchema);
68 | log('debug', '✓ JSON schema validation enabled');
69 | } catch (error) {
70 | log('warn', `⚠ Schema validation disabled: ${error.message}`);
71 | this.validatePrompt = () => true; // Fallback to no validation
72 | }
73 | }
74 |
75 | /**
76 | * Load a prompt template and render it with variables
77 | * @param {string} promptId - The prompt template ID
78 | * @param {Object} variables - Variables to inject into the template
79 | * @param {string} [variantKey] - Optional specific variant to use
80 | * @returns {{systemPrompt: string, userPrompt: string, metadata: Object}}
81 | */
82 | loadPrompt(promptId, variables = {}, variantKey = null) {
83 | try {
84 | // Check cache first
85 | const cacheKey = `${promptId}-${JSON.stringify(variables)}-${variantKey}`;
86 | if (this.cache.has(cacheKey)) {
87 | return this.cache.get(cacheKey);
88 | }
89 |
90 | // Load template
91 | const template = this.loadTemplate(promptId);
92 |
93 | // Validate parameters if schema validation is available
94 | if (this.validatePrompt && this.validatePrompt !== true) {
95 | this.validateParameters(template, variables);
96 | }
97 |
98 | // Select the variant - use specified key or select based on conditions
99 | const variant = variantKey
100 | ? { ...template.prompts[variantKey], name: variantKey }
101 | : this.selectVariant(template, variables);
102 |
103 | // Render the prompts with variables
104 | const rendered = {
105 | systemPrompt: this.renderTemplate(variant.system, variables),
106 | userPrompt: this.renderTemplate(variant.user, variables),
107 | metadata: {
108 | templateId: template.id,
109 | version: template.version,
110 | variant: variant.name || 'default',
111 | parameters: variables
112 | }
113 | };
114 |
115 | // Cache the result
116 | this.cache.set(cacheKey, rendered);
117 |
118 | return rendered;
119 | } catch (error) {
120 | log('error', `Failed to load prompt ${promptId}: ${error.message}`);
121 | throw error;
122 | }
123 | }
124 |
125 | /**
126 | * Load a prompt template from the imported prompts
127 | * @private
128 | */
129 | loadTemplate(promptId) {
130 | // Get template from the map
131 | const template = this.prompts.get(promptId);
132 |
133 | if (!template) {
134 | throw new Error(`Prompt template '${promptId}' not found`);
135 | }
136 |
137 | // Schema validation if available (do this first for detailed errors)
138 | if (this.validatePrompt && this.validatePrompt !== true) {
139 | const valid = this.validatePrompt(template);
140 | if (!valid) {
141 | const errors = this.validatePrompt.errors
142 | .map((err) => `${err.instancePath || 'root'}: ${err.message}`)
143 | .join(', ');
144 | throw new Error(`Schema validation failed: ${errors}`);
145 | }
146 | } else {
147 | // Fallback basic validation if no schema validation available
148 | if (!template.id || !template.prompts || !template.prompts.default) {
149 | throw new Error(
150 | 'Invalid template structure: missing required fields (id, prompts.default)'
151 | );
152 | }
153 | }
154 |
155 | return template;
156 | }
157 |
158 | /**
159 | * Validate parameters against template schema
160 | * @private
161 | */
162 | validateParameters(template, variables) {
163 | if (!template.parameters) return;
164 |
165 | const errors = [];
166 |
167 | for (const [paramName, paramConfig] of Object.entries(
168 | template.parameters
169 | )) {
170 | const value = variables[paramName];
171 |
172 | // Check required parameters
173 | if (paramConfig.required && value === undefined) {
174 | errors.push(`Required parameter '${paramName}' missing`);
175 | continue;
176 | }
177 |
178 | // Skip validation for undefined optional parameters
179 | if (value === undefined) continue;
180 |
181 | // Type validation
182 | if (!this.validateParameterType(value, paramConfig.type)) {
183 | errors.push(
184 | `Parameter '${paramName}' expected ${paramConfig.type}, got ${typeof value}`
185 | );
186 | }
187 |
188 | // Enum validation
189 | if (paramConfig.enum && !paramConfig.enum.includes(value)) {
190 | errors.push(
191 | `Parameter '${paramName}' must be one of: ${paramConfig.enum.join(', ')}`
192 | );
193 | }
194 |
195 | // Pattern validation for strings
196 | if (paramConfig.pattern && typeof value === 'string') {
197 | const regex = new RegExp(paramConfig.pattern);
198 | if (!regex.test(value)) {
199 | errors.push(
200 | `Parameter '${paramName}' does not match required pattern: ${paramConfig.pattern}`
201 | );
202 | }
203 | }
204 |
205 | // Range validation for numbers
206 | if (typeof value === 'number') {
207 | if (paramConfig.minimum !== undefined && value < paramConfig.minimum) {
208 | errors.push(
209 | `Parameter '${paramName}' must be >= ${paramConfig.minimum}`
210 | );
211 | }
212 | if (paramConfig.maximum !== undefined && value > paramConfig.maximum) {
213 | errors.push(
214 | `Parameter '${paramName}' must be <= ${paramConfig.maximum}`
215 | );
216 | }
217 | }
218 | }
219 |
220 | if (errors.length > 0) {
221 | throw new Error(`Parameter validation failed: ${errors.join('; ')}`);
222 | }
223 | }
224 |
225 | /**
226 | * Validate parameter type
227 | * @private
228 | */
229 | validateParameterType(value, expectedType) {
230 | switch (expectedType) {
231 | case 'string':
232 | return typeof value === 'string';
233 | case 'number':
234 | return typeof value === 'number';
235 | case 'boolean':
236 | return typeof value === 'boolean';
237 | case 'array':
238 | return Array.isArray(value);
239 | case 'object':
240 | return (
241 | typeof value === 'object' && value !== null && !Array.isArray(value)
242 | );
243 | default:
244 | return true;
245 | }
246 | }
247 |
248 | /**
249 | * Select the best variant based on conditions
250 | * @private
251 | */
252 | selectVariant(template, variables) {
253 | // Check each variant's condition
254 | for (const [name, variant] of Object.entries(template.prompts)) {
255 | if (name === 'default') continue;
256 |
257 | if (
258 | variant.condition &&
259 | this.evaluateCondition(variant.condition, variables)
260 | ) {
261 | return { ...variant, name };
262 | }
263 | }
264 |
265 | // Fall back to default
266 | return { ...template.prompts.default, name: 'default' };
267 | }
268 |
269 | /**
270 | * Evaluate a condition string
271 | * @private
272 | */
273 | evaluateCondition(condition, variables) {
274 | try {
275 | // Create a safe evaluation context
276 | const context = { ...variables };
277 |
278 | // Simple condition evaluation (can be enhanced)
279 | // For now, supports basic comparisons
280 | const func = new Function(...Object.keys(context), `return ${condition}`);
281 | return func(...Object.values(context));
282 | } catch (error) {
283 | log('warn', `Failed to evaluate condition: ${condition}`);
284 | return false;
285 | }
286 | }
287 |
288 | /**
289 | * Render a template string with variables
290 | * @private
291 | */
292 | renderTemplate(template, variables) {
293 | let rendered = template;
294 |
295 | // Handle helper functions like (eq variable "value")
296 | rendered = rendered.replace(
297 | /\(eq\s+(\w+(?:\.\w+)*)\s+"([^"]+)"\)/g,
298 | (match, path, compareValue) => {
299 | const value = this.getNestedValue(variables, path);
300 | return value === compareValue ? 'true' : 'false';
301 | }
302 | );
303 |
304 | // Handle not helper function like (not variable)
305 | rendered = rendered.replace(/\(not\s+(\w+(?:\.\w+)*)\)/g, (match, path) => {
306 | const value = this.getNestedValue(variables, path);
307 | return !value ? 'true' : 'false';
308 | });
309 |
310 | // Handle gt (greater than) helper function like (gt variable 0)
311 | rendered = rendered.replace(
312 | /\(gt\s+(\w+(?:\.\w+)*)\s+(\d+(?:\.\d+)?)\)/g,
313 | (match, path, compareValue) => {
314 | const value = this.getNestedValue(variables, path);
315 | const numValue = parseFloat(compareValue);
316 | return typeof value === 'number' && value > numValue ? 'true' : 'false';
317 | }
318 | );
319 |
320 | // Handle gte (greater than or equal) helper function like (gte variable 0)
321 | rendered = rendered.replace(
322 | /\(gte\s+(\w+(?:\.\w+)*)\s+(\d+(?:\.\d+)?)\)/g,
323 | (match, path, compareValue) => {
324 | const value = this.getNestedValue(variables, path);
325 | const numValue = parseFloat(compareValue);
326 | return typeof value === 'number' && value >= numValue
327 | ? 'true'
328 | : 'false';
329 | }
330 | );
331 |
332 | // Handle conditionals with else {{#if variable}}...{{else}}...{{/if}}
333 | rendered = rendered.replace(
334 | /\{\{#if\s+([^}]+)\}\}([\s\S]*?)(?:\{\{else\}\}([\s\S]*?))?\{\{\/if\}\}/g,
335 | (match, condition, trueContent, falseContent = '') => {
336 | // Handle boolean values and helper function results
337 | let value;
338 | if (condition === 'true') {
339 | value = true;
340 | } else if (condition === 'false') {
341 | value = false;
342 | } else {
343 | value = this.getNestedValue(variables, condition);
344 | }
345 | return value ? trueContent : falseContent;
346 | }
347 | );
348 |
349 | // Handle each loops {{#each array}}...{{/each}}
350 | rendered = rendered.replace(
351 | /\{\{#each\s+(\w+(?:\.\w+)*)\}\}([\s\S]*?)\{\{\/each\}\}/g,
352 | (match, path, content) => {
353 | const array = this.getNestedValue(variables, path);
354 | if (!Array.isArray(array)) return '';
355 |
356 | return array
357 | .map((item, index) => {
358 | // Create a context with item properties and special variables
359 | const itemContext = {
360 | ...variables,
361 | ...item,
362 | '@index': index,
363 | '@first': index === 0,
364 | '@last': index === array.length - 1
365 | };
366 |
367 | // Recursively render the content with item context
368 | return this.renderTemplate(content, itemContext);
369 | })
370 | .join('');
371 | }
372 | );
373 |
374 | // Handle json helper {{{json variable}}} (triple braces for raw output)
375 | rendered = rendered.replace(
376 | /\{\{\{json\s+(\w+(?:\.\w+)*)\}\}\}/g,
377 | (match, path) => {
378 | const value = this.getNestedValue(variables, path);
379 | return value !== undefined ? JSON.stringify(value, null, 2) : '';
380 | }
381 | );
382 |
383 | // Handle variable substitution {{variable}}
384 | rendered = rendered.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (match, path) => {
385 | const value = this.getNestedValue(variables, path);
386 | return value !== undefined ? value : '';
387 | });
388 |
389 | return rendered;
390 | }
391 |
392 | /**
393 | * Get nested value from object using dot notation
394 | * @private
395 | */
396 | getNestedValue(obj, path) {
397 | return path
398 | .split('.')
399 | .reduce(
400 | (current, key) =>
401 | current && current[key] !== undefined ? current[key] : undefined,
402 | obj
403 | );
404 | }
405 |
406 | /**
407 | * Validate all prompt templates
408 | */
409 | validateAllPrompts() {
410 | const results = { total: 0, errors: [], valid: [] };
411 |
412 | // Iterate through all imported prompts
413 | for (const [promptId, template] of this.prompts.entries()) {
414 | results.total++;
415 |
416 | try {
417 | // Validate the template
418 | if (this.validatePrompt && this.validatePrompt !== true) {
419 | const valid = this.validatePrompt(template);
420 | if (!valid) {
421 | const errors = this.validatePrompt.errors
422 | .map((err) => `${err.instancePath || 'root'}: ${err.message}`)
423 | .join(', ');
424 | throw new Error(`Schema validation failed: ${errors}`);
425 | }
426 | }
427 | results.valid.push(promptId);
428 | } catch (error) {
429 | results.errors.push(`${promptId}: ${error.message}`);
430 | }
431 | }
432 |
433 | return results;
434 | }
435 |
436 | /**
437 | * List all available prompt templates
438 | */
439 | listPrompts() {
440 | const prompts = [];
441 |
442 | // Iterate through all imported prompts
443 | for (const [promptId, template] of this.prompts.entries()) {
444 | try {
445 | prompts.push({
446 | id: template.id,
447 | description: template.description,
448 | version: template.version,
449 | parameters: template.parameters,
450 | tags: template.metadata?.tags || []
451 | });
452 | } catch (error) {
453 | log('warn', `Failed to process template ${promptId}: ${error.message}`);
454 | }
455 | }
456 |
457 | return prompts;
458 | }
459 |
460 | /**
461 | * Validate template structure
462 | * @param {string|Object} templateOrId - Either a template ID or a template object
463 | */
464 | validateTemplate(templateOrId) {
465 | try {
466 | let template;
467 |
468 | // Handle both template ID and direct template object
469 | if (typeof templateOrId === 'string') {
470 | template = this.prompts.get(templateOrId);
471 | if (!template) {
472 | return {
473 | valid: false,
474 | error: `Template '${templateOrId}' not found`
475 | };
476 | }
477 | } else {
478 | template = templateOrId;
479 | }
480 |
481 | // Check required fields
482 | const required = ['id', 'version', 'description', 'prompts'];
483 | for (const field of required) {
484 | if (!template[field]) {
485 | return { valid: false, error: `Missing required field: ${field}` };
486 | }
487 | }
488 |
489 | // Check default prompt exists
490 | if (!template.prompts.default) {
491 | return { valid: false, error: 'Missing default prompt variant' };
492 | }
493 |
494 | // Check each variant has required fields
495 | for (const [name, variant] of Object.entries(template.prompts)) {
496 | if (!variant.system || !variant.user) {
497 | return {
498 | valid: false,
499 | error: `Variant '${name}' missing system or user prompt`
500 | };
501 | }
502 | }
503 |
504 | // Schema validation if available
505 | if (this.validatePrompt && this.validatePrompt !== true) {
506 | const valid = this.validatePrompt(template);
507 | if (!valid) {
508 | const errors = this.validatePrompt.errors
509 | .map((err) => `${err.instancePath || 'root'}: ${err.message}`)
510 | .join(', ');
511 | return { valid: false, error: `Schema validation failed: ${errors}` };
512 | }
513 | }
514 |
515 | return { valid: true };
516 | } catch (error) {
517 | return { valid: false, error: error.message };
518 | }
519 | }
520 | }
521 |
522 | // Singleton instance
523 | let promptManager = null;
524 |
525 | /**
526 | * Get or create the prompt manager instance
527 | * @returns {PromptManager}
528 | */
529 | export function getPromptManager() {
530 | if (!promptManager) {
531 | promptManager = new PromptManager();
532 | }
533 | return promptManager;
534 | }
535 |
```
--------------------------------------------------------------------------------
/docs/command-reference.md:
--------------------------------------------------------------------------------
```markdown
1 | # Task Master Command Reference
2 |
3 | Here's a comprehensive reference of all available commands:
4 |
5 | ## Parse PRD
6 |
7 | ```bash
8 | # Parse a PRD file and generate tasks
9 | task-master parse-prd <prd-file.txt>
10 |
11 | # Limit the number of tasks generated (default is 10)
12 | task-master parse-prd <prd-file.txt> --num-tasks=5
13 |
14 | # Allow task master to determine the number of tasks based on complexity
15 | task-master parse-prd <prd-file.txt> --num-tasks=0
16 | ```
17 |
18 | ## List Tasks
19 |
20 | ```bash
21 | # List all tasks
22 | task-master list
23 |
24 | # List tasks with a specific status
25 | task-master list --status=<status>
26 |
27 | # List tasks with subtasks
28 | task-master list --with-subtasks
29 |
30 | # List tasks with a specific status and include subtasks
31 | task-master list --status=<status> --with-subtasks
32 | ```
33 |
34 | ## Show Next Task
35 |
36 | ```bash
37 | # Show the next task to work on based on dependencies and status
38 | task-master next
39 | ```
40 |
41 | ## Show Specific Task
42 |
43 | ```bash
44 | # Show details of a specific task
45 | task-master show <id>
46 | # or
47 | task-master show --id=<id>
48 |
49 | # View multiple tasks with comma-separated IDs
50 | task-master show 1,3,5
51 | task-master show 44,55
52 |
53 | # View a specific subtask (e.g., subtask 2 of task 1)
54 | task-master show 1.2
55 |
56 | # Mix parent tasks and subtasks
57 | task-master show 44,44.1,55,55.2
58 | ```
59 |
60 | **Multiple Task Display:**
61 |
62 | - **Single ID**: Shows detailed task view with full implementation details
63 | - **Multiple IDs**: Shows compact summary table with interactive action menu
64 | - **Action Menu**: Provides copy-paste ready commands for batch operations:
65 | - Mark all as in-progress/done
66 | - Show next available task
67 | - Expand all tasks (generate subtasks)
68 | - View dependency relationships
69 | - Generate task files
70 |
71 | ## Update Tasks
72 |
73 | ```bash
74 | # Update tasks from a specific ID and provide context
75 | task-master update --from=<id> --prompt="<prompt>"
76 |
77 | # Update tasks using research role
78 | task-master update --from=<id> --prompt="<prompt>" --research
79 | ```
80 |
81 | ## Update a Specific Task
82 |
83 | ```bash
84 | # Update a single task by ID with new information
85 | task-master update-task --id=<id> --prompt="<prompt>"
86 |
87 | # Use research-backed updates
88 | task-master update-task --id=<id> --prompt="<prompt>" --research
89 | ```
90 |
91 | ## Update a Subtask
92 |
93 | ```bash
94 | # Append additional information to a specific subtask
95 | task-master update-subtask --id=<parentId.subtaskId> --prompt="<prompt>"
96 |
97 | # Example: Add details about API rate limiting to subtask 2 of task 5
98 | task-master update-subtask --id=5.2 --prompt="Add rate limiting of 100 requests per minute"
99 |
100 | # Use research-backed updates
101 | task-master update-subtask --id=<parentId.subtaskId> --prompt="<prompt>" --research
102 | ```
103 |
104 | Unlike the `update-task` command which replaces task information, the `update-subtask` command _appends_ new information to the existing subtask details, marking it with a timestamp. This is useful for iteratively enhancing subtasks while preserving the original content.
105 |
106 | ## Generate Task Files
107 |
108 | ```bash
109 | # Generate individual task files from tasks.json
110 | task-master generate
111 | ```
112 |
113 | ## Set Task Status
114 |
115 | ```bash
116 | # Set status of a single task
117 | task-master set-status --id=<id> --status=<status>
118 |
119 | # Set status for multiple tasks
120 | task-master set-status --id=1,2,3 --status=<status>
121 |
122 | # Set status for subtasks
123 | task-master set-status --id=1.1,1.2 --status=<status>
124 | ```
125 |
126 | When marking a task as "done", all of its subtasks will automatically be marked as "done" as well.
127 |
128 | ## Expand Tasks
129 |
130 | ```bash
131 | # Expand a specific task with subtasks
132 | task-master expand --id=<id> --num=<number>
133 |
134 | # Expand a task with a dynamic number of subtasks (ignoring complexity report)
135 | task-master expand --id=<id> --num=0
136 |
137 | # Expand with additional context
138 | task-master expand --id=<id> --prompt="<context>"
139 |
140 | # Expand all pending tasks
141 | task-master expand --all
142 |
143 | # Force regeneration of subtasks for tasks that already have them
144 | task-master expand --all --force
145 |
146 | # Research-backed subtask generation for a specific task
147 | task-master expand --id=<id> --research
148 |
149 | # Research-backed generation for all tasks
150 | task-master expand --all --research
151 | ```
152 |
153 | ## Clear Subtasks
154 |
155 | ```bash
156 | # Clear subtasks from a specific task
157 | task-master clear-subtasks --id=<id>
158 |
159 | # Clear subtasks from multiple tasks
160 | task-master clear-subtasks --id=1,2,3
161 |
162 | # Clear subtasks from all tasks
163 | task-master clear-subtasks --all
164 | ```
165 |
166 | ## Analyze Task Complexity
167 |
168 | ```bash
169 | # Analyze complexity of all tasks
170 | task-master analyze-complexity
171 |
172 | # Save report to a custom location
173 | task-master analyze-complexity --output=my-report.json
174 |
175 | # Use a specific LLM model
176 | task-master analyze-complexity --model=claude-3-opus-20240229
177 |
178 | # Set a custom complexity threshold (1-10)
179 | task-master analyze-complexity --threshold=6
180 |
181 | # Use an alternative tasks file
182 | task-master analyze-complexity --file=custom-tasks.json
183 |
184 | # Use Perplexity AI for research-backed complexity analysis
185 | task-master analyze-complexity --research
186 | ```
187 |
188 | ## View Complexity Report
189 |
190 | ```bash
191 | # Display the task complexity analysis report
192 | task-master complexity-report
193 |
194 | # View a report at a custom location
195 | task-master complexity-report --file=my-report.json
196 | ```
197 |
198 | ## Managing Task Dependencies
199 |
200 | ```bash
201 | # Add a dependency to a task
202 | task-master add-dependency --id=<id> --depends-on=<id>
203 |
204 | # Remove a dependency from a task
205 | task-master remove-dependency --id=<id> --depends-on=<id>
206 |
207 | # Validate dependencies without fixing them
208 | task-master validate-dependencies
209 |
210 | # Find and fix invalid dependencies automatically
211 | task-master fix-dependencies
212 | ```
213 |
214 | ## Move Tasks
215 |
216 | ```bash
217 | # Move a task or subtask to a new position
218 | task-master move --from=<id> --to=<id>
219 |
220 | # Examples:
221 | # Move task to become a subtask
222 | task-master move --from=5 --to=7
223 |
224 | # Move subtask to become a standalone task
225 | task-master move --from=5.2 --to=7
226 |
227 | # Move subtask to a different parent
228 | task-master move --from=5.2 --to=7.3
229 |
230 | # Reorder subtasks within the same parent
231 | task-master move --from=5.2 --to=5.4
232 |
233 | # Move a task to a new ID position (creates placeholder if doesn't exist)
234 | task-master move --from=5 --to=25
235 |
236 | # Move multiple tasks at once (must have the same number of IDs)
237 | task-master move --from=10,11,12 --to=16,17,18
238 | ```
239 |
240 | ## Add a New Task
241 |
242 | ```bash
243 | # Add a new task using AI (main role)
244 | task-master add-task --prompt="Description of the new task"
245 |
246 | # Add a new task using AI (research role)
247 | task-master add-task --prompt="Description of the new task" --research
248 |
249 | # Add a task with dependencies
250 | task-master add-task --prompt="Description" --dependencies=1,2,3
251 |
252 | # Add a task with priority
253 | task-master add-task --prompt="Description" --priority=high
254 | ```
255 |
256 | ## Tag Management
257 |
258 | Task Master supports tagged task lists for multi-context task management. Each tag represents a separate, isolated context for tasks.
259 |
260 | ```bash
261 | # List all available tags with task counts and status
262 | task-master tags
263 |
264 | # List tags with detailed metadata
265 | task-master tags --show-metadata
266 |
267 | # Create a new empty tag
268 | task-master add-tag <tag-name>
269 |
270 | # Create a new tag with a description
271 | task-master add-tag <tag-name> --description="Feature development tasks"
272 |
273 | # Create a tag based on current git branch name
274 | task-master add-tag --from-branch
275 |
276 | # Create a new tag by copying tasks from the current tag
277 | task-master add-tag <new-tag> --copy-from-current
278 |
279 | # Create a new tag by copying from a specific tag
280 | task-master add-tag <new-tag> --copy-from=<source-tag>
281 |
282 | # Switch to a different tag context
283 | task-master use-tag <tag-name>
284 |
285 | # Rename an existing tag
286 | task-master rename-tag <old-name> <new-name>
287 |
288 | # Copy an entire tag to create a new one
289 | task-master copy-tag <source-tag> <target-tag>
290 |
291 | # Copy a tag with a description
292 | task-master copy-tag <source-tag> <target-tag> --description="Copied for testing"
293 |
294 | # Delete a tag and all its tasks (with confirmation)
295 | task-master delete-tag <tag-name>
296 |
297 | # Delete a tag without confirmation prompt
298 | task-master delete-tag <tag-name> --yes
299 | ```
300 |
301 | **Tag Context:**
302 | - All task operations (list, show, add, update, etc.) work within the currently active tag
303 | - Use `--tag=<name>` flag with most commands to operate on a specific tag context
304 | - Tags provide complete isolation - tasks in different tags don't interfere with each other
305 |
306 | ## Initialize a Project
307 |
308 | ```bash
309 | # Initialize a new project with Task Master structure
310 | task-master init
311 |
312 | # Initialize a new project applying specific rules
313 | task-master init --rules cursor,windsurf,vscode
314 | ```
315 |
316 | - The `--rules` flag allows you to specify one or more rule profiles (e.g., `cursor`, `roo`, `windsurf`, `cline`) to apply during initialization.
317 | - If omitted, all available rule profiles are installed by default (claude, cline, codex, cursor, roo, trae, vscode, windsurf).
318 | - You can use multiple comma-separated profiles in a single command.
319 |
320 | ## Manage Rules
321 |
322 | ```bash
323 | # Add rule profiles to your project
324 | # (e.g., .roo/rules, .windsurf/rules)
325 | task-master rules add <profile1,profile2,...>
326 |
327 | # Remove rule sets from your project
328 | task-master rules remove <profile1,profile2,...>
329 |
330 | # Remove rule sets bypassing safety check (dangerous)
331 | task-master rules remove <profile1,profile2,...> --force
332 |
333 | # Launch interactive rules setup to select rules
334 | # (does not re-initialize project or ask about shell aliases)
335 | task-master rules setup
336 | ```
337 |
338 | - Adding rules creates the profile and rules directory (e.g., `.roo/rules`) and copies/initializes the rules.
339 | - Removing rules deletes the profile and rules directory and associated MCP config.
340 | - **Safety Check**: Attempting to remove rule profiles will trigger a critical warning requiring confirmation. Use `--force` to bypass.
341 | - You can use multiple comma-separated rules in a single command.
342 | - The `setup` action launches an interactive prompt to select which rules to apply. The list of rules is always current with the available profiles, and no manual updates are needed. This command does **not** re-initialize your project or affect shell aliases; it only manages rules interactively.
343 |
344 | **Examples:**
345 |
346 | ```bash
347 | task-master rules add windsurf,roo,vscode
348 | task-master rules remove windsurf
349 | task-master rules setup
350 | ```
351 |
352 | ### Interactive Rules Setup
353 |
354 | You can launch the interactive rules setup at any time with:
355 |
356 | ```bash
357 | task-master rules setup
358 | ```
359 |
360 | This command opens a prompt where you can select which rule profiles (e.g., Cursor, Roo, Windsurf) you want to add to your project. This does **not** re-initialize your project or ask about shell aliases; it only manages rules.
361 |
362 | - Use this command to add rule profiles interactively after project creation.
363 | - The same interactive prompt is also used during `init` if you don't specify rules with `--rules`.
364 |
365 | ## Configure AI Models
366 |
367 | ```bash
368 | # View current AI model configuration and API key status
369 | task-master models
370 |
371 | # Set the primary model for generation/updates (provider inferred if known)
372 | task-master models --set-main=claude-3-opus-20240229
373 |
374 | # Set the research model
375 | task-master models --set-research=sonar-pro
376 |
377 | # Set the fallback model
378 | task-master models --set-fallback=claude-3-haiku-20240307
379 |
380 | # Set a custom Ollama model for the main role
381 | task-master models --set-main=my-local-llama --ollama
382 |
383 | # Set a custom OpenRouter model for the research role
384 | task-master models --set-research=google/gemini-pro --openrouter
385 |
386 | # Set Codex CLI model for the main role (uses ChatGPT subscription via OAuth)
387 | task-master models --set-main=gpt-5-codex --codex-cli
388 |
389 | # Set Codex CLI model for the fallback role
390 | task-master models --set-fallback=gpt-5 --codex-cli
391 |
392 | # Run interactive setup to configure models, including custom ones
393 | task-master models --setup
394 | ```
395 |
396 | Configuration is stored in `.taskmaster/config.json` in your project root (legacy `.taskmasterconfig` files are automatically migrated). API keys are still managed via `.env` or MCP configuration. Use `task-master models` without flags to see available built-in models. Use `--setup` for a guided experience.
397 |
398 | State is stored in `.taskmaster/state.json` in your project root. It maintains important information like the current tag. Do not manually edit this file.
399 |
400 | ## Research Fresh Information
401 |
402 | ```bash
403 | # Perform AI-powered research with fresh, up-to-date information
404 | task-master research "What are the latest best practices for JWT authentication in Node.js?"
405 |
406 | # Research with specific task context
407 | task-master research "How to implement OAuth 2.0?" --id=15,16
408 |
409 | # Research with file context for code-aware suggestions
410 | task-master research "How can I optimize this API implementation?" --files=src/api.js,src/auth.js
411 |
412 | # Research with custom context and project tree
413 | task-master research "Best practices for error handling" --context="We're using Express.js" --tree
414 |
415 | # Research with different detail levels
416 | task-master research "React Query v5 migration guide" --detail=high
417 |
418 | # Disable interactive follow-up questions (useful for scripting, is the default for MCP)
419 | # Use a custom tasks file location
420 | task-master research "How to implement this feature?" --file=custom-tasks.json
421 |
422 | # Research within a specific tag context
423 | task-master research "Database optimization strategies" --tag=feature-branch
424 |
425 | # Save research conversation to .taskmaster/docs/research/ directory (for later reference)
426 | task-master research "Database optimization techniques" --save-file
427 |
428 | # Save key findings directly to a task or subtask (recommended for actionable insights)
429 | task-master research "How to implement OAuth?" --save-to=15
430 | task-master research "API optimization strategies" --save-to=15.2
431 |
432 | # Combine context gathering with automatic saving of findings
433 | task-master research "Best practices for this implementation" --id=15,16 --files=src/auth.js --save-to=15.3
434 | ```
435 |
436 | **The research command is a powerful exploration tool that provides:**
437 |
438 | - **Fresh information beyond AI knowledge cutoffs**
439 | - **Project-aware context** from your tasks and files
440 | - **Automatic task discovery** using fuzzy search
441 | - **Multiple detail levels** (low, medium, high)
442 | - **Token counting and cost tracking**
443 | - **Interactive follow-up questions** for deep exploration
444 | - **Flexible save options** (commit findings to tasks or preserve conversations)
445 | - **Iterative discovery** through continuous questioning and refinement
446 |
447 | **Use research frequently to:**
448 |
449 | - Get current best practices before implementing features
450 | - Research new technologies and libraries
451 | - Find solutions to complex problems
452 | - Validate your implementation approaches
453 | - Stay updated with latest security recommendations
454 |
455 | **Interactive Features (CLI):**
456 |
457 | - **Follow-up questions** that maintain conversation context and allow deep exploration
458 | - **Save menu** during or after research with flexible options:
459 | - **Save to task/subtask**: Commit key findings and actionable insights (recommended)
460 | - **Save to file**: Preserve entire conversation for later reference if needed
461 | - **Continue exploring**: Ask more follow-up questions to dig deeper
462 | - **Automatic file naming** with timestamps and query-based slugs when saving conversations
463 |
```
--------------------------------------------------------------------------------
/docs/task-structure.md:
--------------------------------------------------------------------------------
```markdown
1 | # Task Structure
2 |
3 | Tasks in Task Master follow a specific format designed to provide comprehensive information for both humans and AI assistants.
4 |
5 | ## Task Fields in tasks.json
6 |
7 | Tasks in tasks.json have the following structure:
8 |
9 | - `id`: Unique identifier for the task (Example: `1`)
10 | - `title`: Brief, descriptive title of the task (Example: `"Initialize Repo"`)
11 | - `description`: Concise description of what the task involves (Example: `"Create a new repository, set up initial structure."`)
12 | - `status`: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
13 | - `dependencies`: IDs of tasks that must be completed before this task (Example: `[1, 2]`)
14 | - Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
15 | - This helps quickly identify which prerequisite tasks are blocking work
16 | - `priority`: Importance level of the task (Example: `"high"`, `"medium"`, `"low"`)
17 | - `details`: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
18 | - `testStrategy`: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
19 | - `subtasks`: List of smaller, more specific tasks that make up the main task (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
20 |
21 | ## Task File Format
22 |
23 | Individual task files follow this format:
24 |
25 | ```
26 | # Task ID: <id>
27 | # Title: <title>
28 | # Status: <status>
29 | # Dependencies: <comma-separated list of dependency IDs>
30 | # Priority: <priority>
31 | # Description: <brief description>
32 | # Details:
33 | <detailed implementation notes>
34 |
35 | # Test Strategy:
36 | <verification approach>
37 | ```
38 |
39 | ## Features in Detail
40 |
41 | ### Analyzing Task Complexity
42 |
43 | The `analyze-complexity` command:
44 |
45 | - Analyzes each task using AI to assess its complexity on a scale of 1-10
46 | - Recommends optimal number of subtasks based on configured DEFAULT_SUBTASKS
47 | - Generates tailored prompts for expanding each task
48 | - Creates a comprehensive JSON report with ready-to-use commands
49 | - Saves the report to scripts/task-complexity-report.json by default
50 |
51 | The generated report contains:
52 |
53 | - Complexity analysis for each task (scored 1-10)
54 | - Recommended number of subtasks based on complexity
55 | - AI-generated expansion prompts customized for each task
56 | - Ready-to-run expansion commands directly within each task analysis
57 |
58 | ### Viewing Complexity Report
59 |
60 | The `complexity-report` command:
61 |
62 | - Displays a formatted, easy-to-read version of the complexity analysis report
63 | - Shows tasks organized by complexity score (highest to lowest)
64 | - Provides complexity distribution statistics (low, medium, high)
65 | - Highlights tasks recommended for expansion based on threshold score
66 | - Includes ready-to-use expansion commands for each complex task
67 | - If no report exists, offers to generate one on the spot
68 |
69 | ### Smart Task Expansion
70 |
71 | The `expand` command automatically checks for and uses the complexity report:
72 |
73 | When a complexity report exists:
74 |
75 | - Tasks are automatically expanded using the recommended subtask count and prompts
76 | - When expanding all tasks, they're processed in order of complexity (highest first)
77 | - Research-backed generation is preserved from the complexity analysis
78 | - You can still override recommendations with explicit command-line options
79 |
80 | Example workflow:
81 |
82 | ```bash
83 | # Generate the complexity analysis report with research capabilities
84 | task-master analyze-complexity --research
85 |
86 | # Review the report in a readable format
87 | task-master complexity-report
88 |
89 | # Expand tasks using the optimized recommendations
90 | task-master expand --id=8
91 | # or expand all tasks
92 | task-master expand --all
93 | ```
94 |
95 | ### Finding the Next Task
96 |
97 | The `next` command:
98 |
99 | - Identifies tasks that are pending/in-progress and have all dependencies satisfied
100 | - Prioritizes tasks by priority level, dependency count, and task ID
101 | - Displays comprehensive information about the selected task:
102 | - Basic task details (ID, title, priority, dependencies)
103 | - Implementation details
104 | - Subtasks (if they exist)
105 | - Provides contextual suggested actions:
106 | - Command to mark the task as in-progress
107 | - Command to mark the task as done
108 | - Commands for working with subtasks
109 |
110 | ### Viewing Specific Task Details
111 |
112 | The `show` command:
113 |
114 | - Displays comprehensive details about a specific task or subtask
115 | - Shows task status, priority, dependencies, and detailed implementation notes
116 | - For parent tasks, displays all subtasks and their status
117 | - For subtasks, shows parent task relationship
118 | - Provides contextual action suggestions based on the task's state
119 | - Works with both regular tasks and subtasks (using the format taskId.subtaskId)
120 |
121 | ## Best Practices for AI-Driven Development
122 |
123 | 1. **Start with a detailed PRD**: The more detailed your PRD, the better the generated tasks will be.
124 |
125 | 2. **Review generated tasks**: After parsing the PRD, review the tasks to ensure they make sense and have appropriate dependencies.
126 |
127 | 3. **Analyze task complexity**: Use the complexity analysis feature to identify which tasks should be broken down further.
128 |
129 | 4. **Follow the dependency chain**: Always respect task dependencies - the Cursor agent will help with this.
130 |
131 | 5. **Update as you go**: If your implementation diverges from the plan, use the update command to keep future tasks aligned with your current approach.
132 |
133 | 6. **Break down complex tasks**: Use the expand command to break down complex tasks into manageable subtasks.
134 |
135 | 7. **Regenerate task files**: After any updates to tasks.json, regenerate the task files to keep them in sync.
136 |
137 | 8. **Communicate context to the agent**: When asking the Cursor agent to help with a task, provide context about what you're trying to achieve.
138 |
139 | 9. **Validate dependencies**: Periodically run the validate-dependencies command to check for invalid or circular dependencies.
140 |
141 | # Task Structure Documentation
142 |
143 | Task Master uses a structured JSON format to organize and manage tasks. As of version 0.16.2, Task Master introduces **Tagged Task Lists** for multi-context task management while maintaining full backward compatibility.
144 |
145 | ## Tagged Task Lists System
146 |
147 | Task Master now organizes tasks into separate contexts called **tags**. This enables working across multiple contexts such as different branches, environments, or project phases without conflicts.
148 |
149 | ### Data Structure Overview
150 |
151 | **Tagged Format (Current)**:
152 |
153 | ```json
154 | {
155 | "master": {
156 | "tasks": [
157 | { "id": 1, "title": "Setup API", "status": "pending", ... }
158 | ]
159 | },
160 | "feature-branch": {
161 | "tasks": [
162 | { "id": 1, "title": "New Feature", "status": "pending", ... }
163 | ]
164 | }
165 | }
166 | ```
167 |
168 | **Legacy Format (Automatically Migrated)**:
169 |
170 | ```json
171 | {
172 | "tasks": [
173 | { "id": 1, "title": "Setup API", "status": "pending", ... }
174 | ]
175 | }
176 | ```
177 |
178 | ### Tag-based Task Lists (v0.17+) and Compatibility
179 |
180 | - **Seamless Migration**: Existing `tasks.json` files are automatically migrated to use a "master" tag
181 | - **Zero Disruption**: All existing commands continue to work exactly as before
182 | - **Backward Compatibility**: Existing workflows remain unchanged
183 | - **Silent Process**: Migration happens transparently on first use with a friendly notification
184 |
185 | ## Core Task Properties
186 |
187 | Each task within a tag context contains the following properties:
188 |
189 | ### Required Properties
190 |
191 | - **`id`** (number): Unique identifier within the tag context
192 |
193 | ```json
194 | "id": 1
195 | ```
196 |
197 | - **`title`** (string): Brief, descriptive title
198 |
199 | ```json
200 | "title": "Implement user authentication"
201 | ```
202 |
203 | - **`description`** (string): Concise summary of what the task involves
204 |
205 | ```json
206 | "description": "Create a secure authentication system using JWT tokens"
207 | ```
208 |
209 | - **`status`** (string): Current state of the task
210 | - Valid values: `"pending"`, `"in-progress"`, `"done"`, `"review"`, `"deferred"`, `"cancelled"`
211 | ```json
212 | "status": "pending"
213 | ```
214 |
215 | ### Optional Properties
216 |
217 | - **`dependencies`** (array): IDs of prerequisite tasks that must be completed first
218 |
219 | ```json
220 | "dependencies": [2, 3]
221 | ```
222 |
223 | - **`priority`** (string): Importance level
224 |
225 | - Valid values: `"high"`, `"medium"`, `"low"`
226 | - Default: `"medium"`
227 |
228 | ```json
229 | "priority": "high"
230 | ```
231 |
232 | - **`details`** (string): In-depth implementation instructions
233 |
234 | ```json
235 | "details": "Use GitHub OAuth client ID/secret, handle callback, set session token"
236 | ```
237 |
238 | - **`testStrategy`** (string): Verification approach
239 |
240 | ```json
241 | "testStrategy": "Deploy and call endpoint to confirm authentication flow"
242 | ```
243 |
244 | - **`subtasks`** (array): List of smaller, more specific tasks
245 | ```json
246 | "subtasks": [
247 | {
248 | "id": 1,
249 | "title": "Configure OAuth",
250 | "description": "Set up OAuth configuration",
251 | "status": "pending",
252 | "dependencies": [],
253 | "details": "Configure GitHub OAuth app and store credentials"
254 | }
255 | ]
256 | ```
257 |
258 | ## Subtask Structure
259 |
260 | Subtasks follow a similar structure to main tasks but with some differences:
261 |
262 | ### Subtask Properties
263 |
264 | - **`id`** (number): Unique identifier within the parent task
265 | - **`title`** (string): Brief, descriptive title
266 | - **`description`** (string): Concise summary of the subtask
267 | - **`status`** (string): Current state (same values as main tasks)
268 | - **`dependencies`** (array): Can reference other subtasks or main task IDs
269 | - **`details`** (string): Implementation instructions and notes
270 |
271 | ### Subtask Example
272 |
273 | ```json
274 | {
275 | "id": 2,
276 | "title": "Handle OAuth callback",
277 | "description": "Process the OAuth callback and extract user data",
278 | "status": "pending",
279 | "dependencies": [1],
280 | "details": "Parse callback parameters, exchange code for token, fetch user profile"
281 | }
282 | ```
283 |
284 | ## Complete Example
285 |
286 | Here's a complete example showing the tagged task structure:
287 |
288 | ```json
289 | {
290 | "master": {
291 | "tasks": [
292 | {
293 | "id": 1,
294 | "title": "Setup Express Server",
295 | "description": "Initialize and configure Express.js server with middleware",
296 | "status": "done",
297 | "dependencies": [],
298 | "priority": "high",
299 | "details": "Create Express app with CORS, body parser, and error handling",
300 | "testStrategy": "Start server and verify health check endpoint responds",
301 | "subtasks": [
302 | {
303 | "id": 1,
304 | "title": "Initialize npm project",
305 | "description": "Set up package.json and install dependencies",
306 | "status": "done",
307 | "dependencies": [],
308 | "details": "Run npm init, install express, cors, body-parser"
309 | },
310 | {
311 | "id": 2,
312 | "title": "Configure middleware",
313 | "description": "Set up CORS and body parsing middleware",
314 | "status": "done",
315 | "dependencies": [1],
316 | "details": "Add app.use() calls for cors() and express.json()"
317 | }
318 | ]
319 | },
320 | {
321 | "id": 2,
322 | "title": "Implement user authentication",
323 | "description": "Create secure authentication system",
324 | "status": "pending",
325 | "dependencies": [1],
326 | "priority": "high",
327 | "details": "Use JWT tokens for session management",
328 | "testStrategy": "Test login/logout flow with valid and invalid credentials",
329 | "subtasks": []
330 | }
331 | ]
332 | },
333 | "feature-auth": {
334 | "tasks": [
335 | {
336 | "id": 1,
337 | "title": "OAuth Integration",
338 | "description": "Add OAuth authentication support",
339 | "status": "pending",
340 | "dependencies": [],
341 | "priority": "medium",
342 | "details": "Integrate with GitHub OAuth for user authentication",
343 | "testStrategy": "Test OAuth flow with GitHub account",
344 | "subtasks": []
345 | }
346 | ]
347 | }
348 | }
349 | ```
350 |
351 | ## Tag Context Management
352 |
353 | ### Current Tag Resolution
354 |
355 | Task Master automatically determines the current tag context based on:
356 |
357 | 1. **State Configuration**: Current tag stored in `.taskmaster/state.json`
358 | 2. **Default Fallback**: "master" tag when no context is specified
359 | 3. **Future Enhancement**: Git branch-based tag switching (Part 2)
360 |
361 | ### Tag Isolation
362 |
363 | - **Context Separation**: Tasks in different tags are completely isolated
364 | - **Independent Numbering**: Each tag has its own task ID sequence starting from 1
365 | - **Parallel Development**: Multiple team members can work on separate tags without conflicts
366 |
367 | ## Data Validation
368 |
369 | Task Master validates the following aspects of task data:
370 |
371 | ### Required Validations
372 |
373 | - **Unique IDs**: Task IDs must be unique within each tag context
374 | - **Valid Status**: Status values must be from the allowed set
375 | - **Dependency References**: Dependencies must reference existing task IDs within the same tag
376 | - **Subtask IDs**: Subtask IDs must be unique within their parent task
377 |
378 | ### Optional Validations
379 |
380 | - **Circular Dependencies**: System detects and prevents circular dependency chains
381 | - **Priority Values**: Priority must be one of the allowed values if specified
382 | - **Data Types**: All properties must match their expected data types
383 |
384 | ## File Generation
385 |
386 | Task Master can generate individual markdown files for each task based on the JSON structure. These files include:
387 |
388 | - **Task Overview**: ID, title, status, dependencies
389 | - **Tag Context**: Which tag the task belongs to
390 | - **Implementation Details**: Full task details and test strategy
391 | - **Subtask Breakdown**: All subtasks with their current status
392 | - **Dependency Status**: Visual indicators showing which dependencies are complete
393 |
394 | ## Migration Process
395 |
396 | When Task Master encounters a legacy format `tasks.json` file:
397 |
398 | 1. **Detection**: Automatically detects `{"tasks": [...]}` format
399 | 2. **Transformation**: Converts to `{"master": {"tasks": [...]}}` format
400 | 3. **Configuration**: Updates `.taskmaster/config.json` with tagged system settings
401 | 4. **State Creation**: Creates `.taskmaster/state.json` for tag management
402 | 5. **Notification**: Shows one-time friendly notice about the new system
403 | 6. **Preservation**: All existing task data is preserved exactly as-is
404 |
405 | ## Best Practices
406 |
407 | ### Task Organization
408 |
409 | - **Logical Grouping**: Use tags to group related tasks (e.g., by feature, branch, or milestone)
410 | - **Clear Titles**: Use descriptive titles that explain the task's purpose
411 | - **Proper Dependencies**: Define dependencies to ensure correct execution order
412 | - **Detailed Instructions**: Include sufficient detail in the `details` field for implementation
413 |
414 | ### Tag Management
415 |
416 | - **Meaningful Names**: Use descriptive tag names that reflect their purpose
417 | - **Consistent Naming**: Establish naming conventions for tags (e.g., branch names, feature names)
418 | - **Context Switching**: Be aware of which tag context you're working in
419 | - **Isolation Benefits**: Leverage tag isolation to prevent merge conflicts
420 |
421 | ### Subtask Design
422 |
423 | - **Granular Tasks**: Break down complex tasks into manageable subtasks
424 | - **Clear Dependencies**: Define subtask dependencies to show implementation order
425 | - **Implementation Notes**: Use subtask details to track progress and decisions
426 | - **Status Tracking**: Keep subtask status updated as work progresses
427 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/task-manager/set-task-status.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Tests for the set-task-status.js module
3 | */
4 | import { jest } from '@jest/globals';
5 |
6 | // Mock the dependencies before importing the module under test
7 | jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
8 | readJSON: jest.fn(),
9 | writeJSON: jest.fn(),
10 | log: jest.fn(),
11 | CONFIG: {
12 | model: 'mock-claude-model',
13 | maxTokens: 4000,
14 | temperature: 0.7,
15 | debug: false
16 | },
17 | sanitizePrompt: jest.fn((prompt) => prompt),
18 | truncate: jest.fn((text) => text),
19 | isSilentMode: jest.fn(() => false),
20 | findTaskById: jest.fn((tasks, id) =>
21 | tasks.find((t) => t.id === parseInt(id))
22 | ),
23 | ensureTagMetadata: jest.fn((tagObj) => tagObj),
24 | getCurrentTag: jest.fn(() => 'master')
25 | }));
26 |
27 | jest.unstable_mockModule(
28 | '../../../../../scripts/modules/task-manager/generate-task-files.js',
29 | () => ({
30 | default: jest.fn().mockResolvedValue()
31 | })
32 | );
33 |
34 | jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
35 | formatDependenciesWithStatus: jest.fn(),
36 | displayBanner: jest.fn(),
37 | displayTaskList: jest.fn(),
38 | startLoadingIndicator: jest.fn(() => ({ stop: jest.fn() })),
39 | stopLoadingIndicator: jest.fn(),
40 | getStatusWithColor: jest.fn((status) => status)
41 | }));
42 |
43 | jest.unstable_mockModule('../../../../../src/constants/task-status.js', () => ({
44 | isValidTaskStatus: jest.fn((status) =>
45 | [
46 | 'pending',
47 | 'done',
48 | 'in-progress',
49 | 'review',
50 | 'deferred',
51 | 'cancelled'
52 | ].includes(status)
53 | ),
54 | TASK_STATUS_OPTIONS: [
55 | 'pending',
56 | 'done',
57 | 'in-progress',
58 | 'review',
59 | 'deferred',
60 | 'cancelled'
61 | ]
62 | }));
63 |
64 | jest.unstable_mockModule(
65 | '../../../../../scripts/modules/task-manager/update-single-task-status.js',
66 | () => ({
67 | default: jest.fn()
68 | })
69 | );
70 |
71 | jest.unstable_mockModule(
72 | '../../../../../scripts/modules/dependency-manager.js',
73 | () => ({
74 | validateTaskDependencies: jest.fn()
75 | })
76 | );
77 |
78 | jest.unstable_mockModule(
79 | '../../../../../scripts/modules/config-manager.js',
80 | () => ({
81 | getDebugFlag: jest.fn(() => false)
82 | })
83 | );
84 |
85 | // Import the mocked modules
86 | const { readJSON, writeJSON, log, findTaskById } = await import(
87 | '../../../../../scripts/modules/utils.js'
88 | );
89 |
90 | const generateTaskFiles = (
91 | await import(
92 | '../../../../../scripts/modules/task-manager/generate-task-files.js'
93 | )
94 | ).default;
95 |
96 | const updateSingleTaskStatus = (
97 | await import(
98 | '../../../../../scripts/modules/task-manager/update-single-task-status.js'
99 | )
100 | ).default;
101 |
102 | // Import the module under test
103 | const { default: setTaskStatus } = await import(
104 | '../../../../../scripts/modules/task-manager/set-task-status.js'
105 | );
106 |
107 | // Sample data for tests (from main test file) - TAGGED FORMAT
108 | const sampleTasks = {
109 | master: {
110 | tasks: [
111 | {
112 | id: 1,
113 | title: 'Task 1',
114 | description: 'First task description',
115 | status: 'pending',
116 | dependencies: [],
117 | priority: 'high',
118 | details: 'Detailed information for task 1',
119 | testStrategy: 'Test strategy for task 1'
120 | },
121 | {
122 | id: 2,
123 | title: 'Task 2',
124 | description: 'Second task description',
125 | status: 'pending',
126 | dependencies: [1],
127 | priority: 'medium',
128 | details: 'Detailed information for task 2',
129 | testStrategy: 'Test strategy for task 2'
130 | },
131 | {
132 | id: 3,
133 | title: 'Task with Subtasks',
134 | description: 'Task with subtasks description',
135 | status: 'pending',
136 | dependencies: [1, 2],
137 | priority: 'high',
138 | details: 'Detailed information for task 3',
139 | testStrategy: 'Test strategy for task 3',
140 | subtasks: [
141 | {
142 | id: 1,
143 | title: 'Subtask 1',
144 | description: 'First subtask',
145 | status: 'pending',
146 | dependencies: [],
147 | details: 'Details for subtask 1'
148 | },
149 | {
150 | id: 2,
151 | title: 'Subtask 2',
152 | description: 'Second subtask',
153 | status: 'pending',
154 | dependencies: [1],
155 | details: 'Details for subtask 2'
156 | }
157 | ]
158 | }
159 | ]
160 | }
161 | };
162 |
163 | describe('setTaskStatus', () => {
164 | beforeEach(() => {
165 | jest.clearAllMocks();
166 |
167 | // Mock console methods to suppress output
168 | jest.spyOn(console, 'log').mockImplementation(() => {});
169 | jest.spyOn(console, 'error').mockImplementation(() => {});
170 |
171 | // Mock process.exit to prevent actual exit
172 | jest.spyOn(process, 'exit').mockImplementation((code) => {
173 | throw new Error(`process.exit: ${code}`);
174 | });
175 |
176 | // Set up updateSingleTaskStatus mock to actually update the data
177 | updateSingleTaskStatus.mockImplementation(
178 | async (tasksPath, taskId, newStatus, data) => {
179 | // This mock now operates on the tasks array passed in the `data` object
180 | const { tasks } = data;
181 | // Handle subtask notation (e.g., "3.1")
182 | if (taskId.includes('.')) {
183 | const [parentId, subtaskId] = taskId
184 | .split('.')
185 | .map((id) => parseInt(id, 10));
186 | const parentTask = tasks.find((t) => t.id === parentId);
187 | if (!parentTask) {
188 | throw new Error(`Parent task ${parentId} not found`);
189 | }
190 | if (!parentTask.subtasks) {
191 | throw new Error(`Parent task ${parentId} has no subtasks`);
192 | }
193 | const subtask = parentTask.subtasks.find((st) => st.id === subtaskId);
194 | if (!subtask) {
195 | throw new Error(
196 | `Subtask ${subtaskId} not found in parent task ${parentId}`
197 | );
198 | }
199 | subtask.status = newStatus;
200 | } else {
201 | // Handle regular task
202 | const task = tasks.find((t) => t.id === parseInt(taskId, 10));
203 | if (!task) {
204 | throw new Error(`Task ${taskId} not found`);
205 | }
206 | task.status = newStatus;
207 |
208 | // If marking parent as done, mark all subtasks as done too
209 | if (newStatus === 'done' && task.subtasks) {
210 | task.subtasks.forEach((subtask) => {
211 | subtask.status = 'done';
212 | });
213 | }
214 | }
215 | }
216 | );
217 | });
218 |
219 | afterEach(() => {
220 | // Restore console methods
221 | jest.restoreAllMocks();
222 | });
223 |
224 | test('should update task status in tasks.json', async () => {
225 | // Arrange
226 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
227 | const tasksPath = '/mock/path/tasks.json';
228 |
229 | readJSON.mockReturnValue({
230 | ...testTasksData.master,
231 | tag: 'master',
232 | _rawTaggedData: testTasksData
233 | });
234 |
235 | // Act
236 | await setTaskStatus(tasksPath, '2', 'done', {
237 | tag: 'master',
238 | mcpLog: { info: jest.fn() }
239 | });
240 |
241 | // Assert
242 | expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master');
243 | expect(writeJSON).toHaveBeenCalledWith(
244 | tasksPath,
245 | expect.objectContaining({
246 | master: expect.objectContaining({
247 | tasks: expect.arrayContaining([
248 | expect.objectContaining({ id: 2, status: 'done' })
249 | ])
250 | })
251 | }),
252 | undefined,
253 | 'master'
254 | );
255 | // expect(generateTaskFiles).toHaveBeenCalledWith(
256 | // tasksPath,
257 | // expect.any(String),
258 | // expect.any(Object)
259 | // );
260 | });
261 |
262 | test('should update subtask status when using dot notation', async () => {
263 | // Arrange
264 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
265 | const tasksPath = '/mock/path/tasks.json';
266 |
267 | readJSON.mockReturnValue({
268 | ...testTasksData.master,
269 | tag: 'master',
270 | _rawTaggedData: testTasksData
271 | });
272 |
273 | // Act
274 | await setTaskStatus(tasksPath, '3.1', 'done', {
275 | tag: 'master',
276 | mcpLog: { info: jest.fn() }
277 | });
278 |
279 | // Assert
280 | expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master');
281 | expect(writeJSON).toHaveBeenCalledWith(
282 | tasksPath,
283 | expect.objectContaining({
284 | master: expect.objectContaining({
285 | tasks: expect.arrayContaining([
286 | expect.objectContaining({
287 | id: 3,
288 | subtasks: expect.arrayContaining([
289 | expect.objectContaining({ id: 1, status: 'done' })
290 | ])
291 | })
292 | ])
293 | })
294 | }),
295 | undefined,
296 | 'master'
297 | );
298 | });
299 |
300 | test('should update multiple tasks when given comma-separated IDs', async () => {
301 | // Arrange
302 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
303 | const tasksPath = '/mock/path/tasks.json';
304 |
305 | readJSON.mockReturnValue({
306 | ...testTasksData.master,
307 | tag: 'master',
308 | _rawTaggedData: testTasksData
309 | });
310 |
311 | // Act
312 | await setTaskStatus(tasksPath, '1,2', 'done', {
313 | tag: 'master',
314 | mcpLog: { info: jest.fn() }
315 | });
316 |
317 | // Assert
318 | expect(readJSON).toHaveBeenCalledWith(tasksPath, undefined, 'master');
319 | expect(writeJSON).toHaveBeenCalledWith(
320 | tasksPath,
321 | expect.objectContaining({
322 | master: expect.objectContaining({
323 | tasks: expect.arrayContaining([
324 | expect.objectContaining({ id: 1, status: 'done' }),
325 | expect.objectContaining({ id: 2, status: 'done' })
326 | ])
327 | })
328 | }),
329 | undefined,
330 | 'master'
331 | );
332 | });
333 |
334 | test('should automatically mark subtasks as done when parent is marked done', async () => {
335 | // Arrange
336 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
337 | const tasksPath = '/mock/path/tasks.json';
338 |
339 | readJSON.mockReturnValue({
340 | ...testTasksData.master,
341 | tag: 'master',
342 | _rawTaggedData: testTasksData
343 | });
344 |
345 | // Act
346 | await setTaskStatus(tasksPath, '3', 'done', {
347 | tag: 'master',
348 | mcpLog: { info: jest.fn() }
349 | });
350 |
351 | // Assert
352 | expect(writeJSON).toHaveBeenCalledWith(
353 | tasksPath,
354 | expect.objectContaining({
355 | master: expect.objectContaining({
356 | tasks: expect.arrayContaining([
357 | expect.objectContaining({
358 | id: 3,
359 | status: 'done',
360 | subtasks: expect.arrayContaining([
361 | expect.objectContaining({ id: 1, status: 'done' }),
362 | expect.objectContaining({ id: 2, status: 'done' })
363 | ])
364 | })
365 | ])
366 | })
367 | }),
368 | undefined,
369 | 'master'
370 | );
371 | });
372 |
373 | test('should throw error for non-existent task ID', async () => {
374 | // Arrange
375 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
376 | const tasksPath = '/mock/path/tasks.json';
377 |
378 | readJSON.mockReturnValue({
379 | ...testTasksData.master,
380 | tag: 'master',
381 | _rawTaggedData: testTasksData
382 | });
383 |
384 | // Act & Assert
385 | await expect(
386 | setTaskStatus(tasksPath, '99', 'done', {
387 | tag: 'master',
388 | mcpLog: { info: jest.fn() }
389 | })
390 | ).rejects.toThrow('Task 99 not found');
391 | });
392 |
393 | test('should throw error for invalid status', async () => {
394 | // Arrange
395 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
396 | const tasksPath = '/mock/path/tasks.json';
397 |
398 | readJSON.mockReturnValue({
399 | ...testTasksData.master,
400 | tag: 'master',
401 | _rawTaggedData: testTasksData
402 | });
403 |
404 | // Act & Assert
405 | await expect(
406 | setTaskStatus(tasksPath, '2', 'InvalidStatus', {
407 | mcpLog: { info: jest.fn() }
408 | })
409 | ).rejects.toThrow(/Invalid status value: InvalidStatus/);
410 | });
411 |
412 | test('should handle parent tasks without subtasks when updating subtask', async () => {
413 | // Arrange
414 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
415 | // Remove subtasks from task 3
416 | const { subtasks, ...taskWithoutSubtasks } = testTasksData.master.tasks[2];
417 | testTasksData.master.tasks[2] = taskWithoutSubtasks;
418 |
419 | const tasksPath = '/mock/path/tasks.json';
420 | readJSON.mockReturnValue({
421 | ...testTasksData.master,
422 | tag: 'master',
423 | _rawTaggedData: testTasksData
424 | });
425 |
426 | // Act & Assert
427 | await expect(
428 | setTaskStatus(tasksPath, '3.1', 'done', {
429 | tag: 'master',
430 | mcpLog: { info: jest.fn() }
431 | })
432 | ).rejects.toThrow('has no subtasks');
433 | });
434 |
435 | test('should handle non-existent subtask ID', async () => {
436 | // Arrange
437 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
438 | const tasksPath = '/mock/path/tasks.json';
439 |
440 | readJSON.mockReturnValue({
441 | ...testTasksData.master,
442 | tag: 'master',
443 | _rawTaggedData: testTasksData
444 | });
445 |
446 | // Act & Assert
447 | await expect(
448 | setTaskStatus(tasksPath, '3.99', 'done', {
449 | tag: 'master',
450 | mcpLog: { info: jest.fn() }
451 | })
452 | ).rejects.toThrow('Subtask 99 not found');
453 | });
454 |
455 | test('should handle file read errors', async () => {
456 | // Arrange
457 | const tasksPath = 'tasks/tasks.json';
458 | const taskId = '2';
459 | const newStatus = 'done';
460 |
461 | readJSON.mockImplementation(() => {
462 | throw new Error('File not found');
463 | });
464 |
465 | // Act & Assert
466 | await expect(
467 | setTaskStatus(tasksPath, taskId, newStatus, {
468 | mcpLog: { info: jest.fn() }
469 | })
470 | ).rejects.toThrow('File not found');
471 |
472 | // Verify that writeJSON was not called due to read error
473 | expect(writeJSON).not.toHaveBeenCalled();
474 | });
475 |
476 | test('should handle empty task ID input', async () => {
477 | // Arrange
478 | const tasksPath = 'tasks/tasks.json';
479 | const emptyTaskId = '';
480 | const newStatus = 'done';
481 |
482 | // Act & Assert
483 | await expect(
484 | setTaskStatus(tasksPath, emptyTaskId, newStatus, {
485 | mcpLog: { info: jest.fn() }
486 | })
487 | ).rejects.toThrow();
488 |
489 | // Verify that updateSingleTaskStatus was not called
490 | expect(updateSingleTaskStatus).not.toHaveBeenCalled();
491 | });
492 |
493 | test('should handle whitespace in comma-separated IDs', async () => {
494 | // Arrange
495 | const testTasksData = JSON.parse(JSON.stringify(sampleTasks));
496 | const tasksPath = 'tasks/tasks.json';
497 | const taskIds = ' 1 , 2 , 3 '; // IDs with whitespace
498 | const newStatus = 'in-progress';
499 |
500 | readJSON.mockReturnValue({
501 | ...testTasksData.master,
502 | tag: 'master',
503 | _rawTaggedData: testTasksData
504 | });
505 |
506 | // Act
507 | const result = await setTaskStatus(tasksPath, taskIds, newStatus, {
508 | tag: 'master',
509 | mcpLog: { info: jest.fn() }
510 | });
511 |
512 | // Assert
513 | expect(updateSingleTaskStatus).toHaveBeenCalledTimes(3);
514 | expect(updateSingleTaskStatus).toHaveBeenCalledWith(
515 | tasksPath,
516 | '1',
517 | newStatus,
518 | expect.objectContaining({
519 | tasks: expect.any(Array),
520 | tag: 'master',
521 | _rawTaggedData: expect.any(Object)
522 | }),
523 | false
524 | );
525 | expect(updateSingleTaskStatus).toHaveBeenCalledWith(
526 | tasksPath,
527 | '2',
528 | newStatus,
529 | expect.objectContaining({
530 | tasks: expect.any(Array),
531 | tag: 'master',
532 | _rawTaggedData: expect.any(Object)
533 | }),
534 | false
535 | );
536 | expect(updateSingleTaskStatus).toHaveBeenCalledWith(
537 | tasksPath,
538 | '3',
539 | newStatus,
540 | expect.objectContaining({
541 | tasks: expect.any(Array),
542 | tag: 'master',
543 | _rawTaggedData: expect.any(Object)
544 | }),
545 | false
546 | );
547 | expect(result).toBeDefined();
548 | });
549 |
550 | // Regression test to ensure tag preservation when updating in multi-tag environment
551 | test('should preserve other tags when updating task status', async () => {
552 | // Arrange
553 | const multiTagData = {
554 | master: JSON.parse(JSON.stringify(sampleTasks.master)),
555 | 'feature-branch': {
556 | tasks: [
557 | { id: 10, title: 'FB Task', status: 'pending', dependencies: [] }
558 | ],
559 | metadata: { description: 'Feature branch tasks' }
560 | }
561 | };
562 | const tasksPath = '/mock/path/tasks.json';
563 |
564 | readJSON.mockReturnValue({
565 | ...multiTagData.master, // resolved view not used
566 | tag: 'master',
567 | _rawTaggedData: multiTagData
568 | });
569 |
570 | // Act
571 | await setTaskStatus(tasksPath, '1', 'done', {
572 | tag: 'master',
573 | mcpLog: { info: jest.fn() }
574 | });
575 |
576 | // Assert: writeJSON should be called with data containing both tags intact
577 | const writeArgs = writeJSON.mock.calls[0];
578 | expect(writeArgs[0]).toBe(tasksPath);
579 | const writtenData = writeArgs[1];
580 | expect(writtenData).toHaveProperty('master');
581 | expect(writtenData).toHaveProperty('feature-branch');
582 | // master task updated
583 | const updatedTask = writtenData.master.tasks.find((t) => t.id === 1);
584 | expect(updatedTask.status).toBe('done');
585 | // feature-branch untouched
586 | expect(writtenData['feature-branch'].tasks[0].status).toBe('pending');
587 | // ensure additional args (projectRoot undefined, tag 'master') present
588 | expect(writeArgs[2]).toBeUndefined();
589 | expect(writeArgs[3]).toBe('master');
590 | });
591 | });
592 |
```
--------------------------------------------------------------------------------
/tests/integration/manage-gitignore.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Integration tests for manage-gitignore.js module
3 | * Tests actual file system operations in a temporary directory
4 | */
5 |
6 | import fs from 'fs';
7 | import path from 'path';
8 | import os from 'os';
9 | import manageGitignoreFile from '../../src/utils/manage-gitignore.js';
10 |
11 | describe('manage-gitignore.js Integration Tests', () => {
12 | let tempDir;
13 | let testGitignorePath;
14 |
15 | beforeEach(() => {
16 | // Create a temporary directory for each test
17 | tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitignore-test-'));
18 | testGitignorePath = path.join(tempDir, '.gitignore');
19 | });
20 |
21 | afterEach(() => {
22 | // Clean up temporary directory after each test
23 | if (fs.existsSync(tempDir)) {
24 | fs.rmSync(tempDir, { recursive: true, force: true });
25 | }
26 | });
27 |
28 | describe('New File Creation', () => {
29 | const templateContent = `# Logs
30 | logs
31 | *.log
32 | npm-debug.log*
33 |
34 | # Dependencies
35 | node_modules/
36 | jspm_packages/
37 |
38 | # Environment variables
39 | .env
40 | .env.local
41 |
42 | # Task files
43 | tasks.json
44 | tasks/ `;
45 |
46 | test('should create new .gitignore file with commented task lines (storeTasksInGit = true)', () => {
47 | const logs = [];
48 | const mockLog = (level, message) => logs.push({ level, message });
49 |
50 | manageGitignoreFile(testGitignorePath, templateContent, true, mockLog);
51 |
52 | // Verify file was created
53 | expect(fs.existsSync(testGitignorePath)).toBe(true);
54 |
55 | // Verify content
56 | const content = fs.readFileSync(testGitignorePath, 'utf8');
57 | expect(content).toContain('# Logs');
58 | expect(content).toContain('logs');
59 | expect(content).toContain('# Dependencies');
60 | expect(content).toContain('node_modules/');
61 | expect(content).toContain('# Task files');
62 | expect(content).toContain('tasks.json');
63 | expect(content).toContain('tasks/');
64 |
65 | // Verify task lines are commented (storeTasksInGit = true)
66 | expect(content).toMatch(
67 | /# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
68 | );
69 |
70 | // Verify log message
71 | expect(logs).toContainEqual({
72 | level: 'success',
73 | message: expect.stringContaining('Created')
74 | });
75 | });
76 |
77 | test('should create new .gitignore file with uncommented task lines (storeTasksInGit = false)', () => {
78 | const logs = [];
79 | const mockLog = (level, message) => logs.push({ level, message });
80 |
81 | manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
82 |
83 | // Verify file was created
84 | expect(fs.existsSync(testGitignorePath)).toBe(true);
85 |
86 | // Verify content
87 | const content = fs.readFileSync(testGitignorePath, 'utf8');
88 | expect(content).toContain('# Task files');
89 |
90 | // Verify task lines are uncommented (storeTasksInGit = false)
91 | expect(content).toMatch(
92 | /# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
93 | );
94 |
95 | // Verify log message
96 | expect(logs).toContainEqual({
97 | level: 'success',
98 | message: expect.stringContaining('Created')
99 | });
100 | });
101 |
102 | test('should work without log function', () => {
103 | expect(() => {
104 | manageGitignoreFile(testGitignorePath, templateContent, false);
105 | }).not.toThrow();
106 |
107 | expect(fs.existsSync(testGitignorePath)).toBe(true);
108 | });
109 | });
110 |
111 | describe('File Merging', () => {
112 | const templateContent = `# Logs
113 | logs
114 | *.log
115 |
116 | # Dependencies
117 | node_modules/
118 |
119 | # Environment variables
120 | .env
121 |
122 | # Task files
123 | tasks.json
124 | tasks/ `;
125 |
126 | test('should merge template with existing file content', () => {
127 | // Create existing .gitignore file
128 | const existingContent = `# Existing content
129 | old-files.txt
130 | *.backup
131 |
132 | # Old task files (to be replaced)
133 | # Task files
134 | # tasks.json
135 | # tasks/
136 |
137 | # More existing content
138 | cache/`;
139 |
140 | fs.writeFileSync(testGitignorePath, existingContent);
141 |
142 | const logs = [];
143 | const mockLog = (level, message) => logs.push({ level, message });
144 |
145 | manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
146 |
147 | // Verify file still exists
148 | expect(fs.existsSync(testGitignorePath)).toBe(true);
149 |
150 | const content = fs.readFileSync(testGitignorePath, 'utf8');
151 |
152 | // Should retain existing non-task content
153 | expect(content).toContain('# Existing content');
154 | expect(content).toContain('old-files.txt');
155 | expect(content).toContain('*.backup');
156 | expect(content).toContain('# More existing content');
157 | expect(content).toContain('cache/');
158 |
159 | // Should add new template content
160 | expect(content).toContain('# Logs');
161 | expect(content).toContain('logs');
162 | expect(content).toContain('# Dependencies');
163 | expect(content).toContain('node_modules/');
164 | expect(content).toContain('# Environment variables');
165 | expect(content).toContain('.env');
166 |
167 | // Should replace task section with new preference (storeTasksInGit = false means uncommented)
168 | expect(content).toMatch(
169 | /# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
170 | );
171 |
172 | // Verify log message
173 | expect(logs).toContainEqual({
174 | level: 'success',
175 | message: expect.stringContaining('Updated')
176 | });
177 | });
178 |
179 | test('should handle switching task preferences from commented to uncommented', () => {
180 | // Create existing file with commented task lines
181 | const existingContent = `# Existing
182 | existing.txt
183 |
184 | # Task files
185 | # tasks.json
186 | # tasks/ `;
187 |
188 | fs.writeFileSync(testGitignorePath, existingContent);
189 |
190 | // Update with storeTasksInGit = true (commented)
191 | manageGitignoreFile(testGitignorePath, templateContent, true);
192 |
193 | const content = fs.readFileSync(testGitignorePath, 'utf8');
194 |
195 | // Should retain existing content
196 | expect(content).toContain('# Existing');
197 | expect(content).toContain('existing.txt');
198 |
199 | // Should have commented task lines (storeTasksInGit = true)
200 | expect(content).toMatch(
201 | /# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
202 | );
203 | });
204 |
205 | test('should handle switching task preferences from uncommented to commented', () => {
206 | // Create existing file with uncommented task lines
207 | const existingContent = `# Existing
208 | existing.txt
209 |
210 | # Task files
211 | tasks.json
212 | tasks/ `;
213 |
214 | fs.writeFileSync(testGitignorePath, existingContent);
215 |
216 | // Update with storeTasksInGit = false (uncommented)
217 | manageGitignoreFile(testGitignorePath, templateContent, false);
218 |
219 | const content = fs.readFileSync(testGitignorePath, 'utf8');
220 |
221 | // Should retain existing content
222 | expect(content).toContain('# Existing');
223 | expect(content).toContain('existing.txt');
224 |
225 | // Should have uncommented task lines (storeTasksInGit = false)
226 | expect(content).toMatch(
227 | /# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
228 | );
229 | });
230 |
231 | test('should not duplicate existing template content', () => {
232 | // Create existing file that already has some template content
233 | const existingContent = `# Logs
234 | logs
235 | *.log
236 |
237 | # Dependencies
238 | node_modules/
239 |
240 | # Custom content
241 | custom.txt
242 |
243 | # Task files
244 | # tasks.json
245 | # tasks/ `;
246 |
247 | fs.writeFileSync(testGitignorePath, existingContent);
248 |
249 | manageGitignoreFile(testGitignorePath, templateContent, false);
250 |
251 | const content = fs.readFileSync(testGitignorePath, 'utf8');
252 |
253 | // Should not duplicate logs section
254 | const logsMatches = content.match(/# Logs/g);
255 | expect(logsMatches).toHaveLength(1);
256 |
257 | // Should not duplicate dependencies section
258 | const depsMatches = content.match(/# Dependencies/g);
259 | expect(depsMatches).toHaveLength(1);
260 |
261 | // Should retain custom content
262 | expect(content).toContain('# Custom content');
263 | expect(content).toContain('custom.txt');
264 |
265 | // Should add new template content that wasn't present
266 | expect(content).toContain('# Environment variables');
267 | expect(content).toContain('.env');
268 | });
269 |
270 | test('should handle empty existing file', () => {
271 | // Create empty file
272 | fs.writeFileSync(testGitignorePath, '');
273 |
274 | manageGitignoreFile(testGitignorePath, templateContent, false);
275 |
276 | expect(fs.existsSync(testGitignorePath)).toBe(true);
277 |
278 | const content = fs.readFileSync(testGitignorePath, 'utf8');
279 | expect(content).toContain('# Logs');
280 | expect(content).toContain('# Task files');
281 | expect(content).toMatch(
282 | /# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
283 | );
284 | });
285 |
286 | test('should handle file with only whitespace', () => {
287 | // Create file with only whitespace
288 | fs.writeFileSync(testGitignorePath, ' \n\n \n');
289 |
290 | manageGitignoreFile(testGitignorePath, templateContent, true);
291 |
292 | const content = fs.readFileSync(testGitignorePath, 'utf8');
293 | expect(content).toContain('# Logs');
294 | expect(content).toContain('# Task files');
295 | expect(content).toMatch(
296 | /# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
297 | );
298 | });
299 | });
300 |
301 | describe('Complex Task Section Handling', () => {
302 | test('should remove task section with mixed comments and spacing', () => {
303 | const existingContent = `# Dependencies
304 | node_modules/
305 |
306 | # Task files
307 |
308 | # tasks.json
309 | tasks/
310 |
311 |
312 | # More content
313 | more.txt`;
314 |
315 | const templateContent = `# New content
316 | new.txt
317 |
318 | # Task files
319 | tasks.json
320 | tasks/ `;
321 |
322 | fs.writeFileSync(testGitignorePath, existingContent);
323 |
324 | manageGitignoreFile(testGitignorePath, templateContent, false);
325 |
326 | const content = fs.readFileSync(testGitignorePath, 'utf8');
327 |
328 | // Should retain non-task content
329 | expect(content).toContain('# Dependencies');
330 | expect(content).toContain('node_modules/');
331 | expect(content).toContain('# More content');
332 | expect(content).toContain('more.txt');
333 |
334 | // Should add new content
335 | expect(content).toContain('# New content');
336 | expect(content).toContain('new.txt');
337 |
338 | // Should have clean task section (storeTasksInGit = false means uncommented)
339 | expect(content).toMatch(
340 | /# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
341 | );
342 | });
343 |
344 | test('should handle multiple task file variations', () => {
345 | const existingContent = `# Existing
346 | existing.txt
347 |
348 | # Task files
349 | tasks.json
350 | # tasks.json
351 | # tasks/
352 | tasks/
353 | #tasks.json
354 |
355 | # More content
356 | more.txt`;
357 |
358 | const templateContent = `# Task files
359 | tasks.json
360 | tasks/ `;
361 |
362 | fs.writeFileSync(testGitignorePath, existingContent);
363 |
364 | manageGitignoreFile(testGitignorePath, templateContent, true);
365 |
366 | const content = fs.readFileSync(testGitignorePath, 'utf8');
367 |
368 | // Should retain non-task content
369 | expect(content).toContain('# Existing');
370 | expect(content).toContain('existing.txt');
371 | expect(content).toContain('# More content');
372 | expect(content).toContain('more.txt');
373 |
374 | // Should have clean task section with preference applied (storeTasksInGit = true means commented)
375 | expect(content).toMatch(
376 | /# Task files\s*[\r\n]+# tasks\.json\s*[\r\n]+# tasks\/ /
377 | );
378 |
379 | // Should not have multiple task sections
380 | const taskFileMatches = content.match(/# Task files/g);
381 | expect(taskFileMatches).toHaveLength(1);
382 | });
383 | });
384 |
385 | describe('Error Handling', () => {
386 | test('should handle permission errors gracefully', () => {
387 | // Create a directory where we would create the file, then remove write permissions
388 | const readOnlyDir = path.join(tempDir, 'readonly');
389 | fs.mkdirSync(readOnlyDir);
390 | fs.chmodSync(readOnlyDir, 0o444); // Read-only
391 |
392 | const readOnlyGitignorePath = path.join(readOnlyDir, '.gitignore');
393 | const templateContent = `# Test
394 | test.txt
395 |
396 | # Task files
397 | tasks.json
398 | tasks/ `;
399 |
400 | const logs = [];
401 | const mockLog = (level, message) => logs.push({ level, message });
402 |
403 | expect(() => {
404 | manageGitignoreFile(
405 | readOnlyGitignorePath,
406 | templateContent,
407 | false,
408 | mockLog
409 | );
410 | }).toThrow();
411 |
412 | // Verify error was logged
413 | expect(logs).toContainEqual({
414 | level: 'error',
415 | message: expect.stringContaining('Failed to create')
416 | });
417 |
418 | // Restore permissions for cleanup
419 | fs.chmodSync(readOnlyDir, 0o755);
420 | });
421 |
422 | test('should handle read errors on existing files', () => {
423 | // Create a file then remove read permissions
424 | fs.writeFileSync(testGitignorePath, 'existing content');
425 | fs.chmodSync(testGitignorePath, 0o000); // No permissions
426 |
427 | const templateContent = `# Test
428 | test.txt
429 |
430 | # Task files
431 | tasks.json
432 | tasks/ `;
433 |
434 | const logs = [];
435 | const mockLog = (level, message) => logs.push({ level, message });
436 |
437 | expect(() => {
438 | manageGitignoreFile(testGitignorePath, templateContent, false, mockLog);
439 | }).toThrow();
440 |
441 | // Verify error was logged
442 | expect(logs).toContainEqual({
443 | level: 'error',
444 | message: expect.stringContaining('Failed to merge content')
445 | });
446 |
447 | // Restore permissions for cleanup
448 | fs.chmodSync(testGitignorePath, 0o644);
449 | });
450 | });
451 |
452 | describe('Real-world Scenarios', () => {
453 | test('should handle typical Node.js project .gitignore', () => {
454 | const existingNodeGitignore = `# Logs
455 | logs
456 | *.log
457 | npm-debug.log*
458 | yarn-debug.log*
459 | yarn-error.log*
460 |
461 | # Runtime data
462 | pids
463 | *.pid
464 | *.seed
465 | *.pid.lock
466 |
467 | # Dependency directories
468 | node_modules/
469 | jspm_packages/
470 |
471 | # Optional npm cache directory
472 | .npm
473 |
474 | # Output of 'npm pack'
475 | *.tgz
476 |
477 | # Yarn Integrity file
478 | .yarn-integrity
479 |
480 | # dotenv environment variables file
481 | .env
482 |
483 | # next.js build output
484 | .next`;
485 |
486 | const taskMasterTemplate = `# Logs
487 | logs
488 | *.log
489 |
490 | # Dependencies
491 | node_modules/
492 |
493 | # Environment variables
494 | .env
495 |
496 | # Build output
497 | dist/
498 | build/
499 |
500 | # Task files
501 | tasks.json
502 | tasks/ `;
503 |
504 | fs.writeFileSync(testGitignorePath, existingNodeGitignore);
505 |
506 | manageGitignoreFile(testGitignorePath, taskMasterTemplate, false);
507 |
508 | const content = fs.readFileSync(testGitignorePath, 'utf8');
509 |
510 | // Should retain existing Node.js specific entries
511 | expect(content).toContain('npm-debug.log*');
512 | expect(content).toContain('yarn-debug.log*');
513 | expect(content).toContain('*.pid');
514 | expect(content).toContain('jspm_packages/');
515 | expect(content).toContain('.npm');
516 | expect(content).toContain('*.tgz');
517 | expect(content).toContain('.yarn-integrity');
518 | expect(content).toContain('.next');
519 |
520 | // Should add new content from template that wasn't present
521 | expect(content).toContain('dist/');
522 | expect(content).toContain('build/');
523 |
524 | // Should add task files section with correct preference (storeTasksInGit = false means uncommented)
525 | expect(content).toMatch(
526 | /# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
527 | );
528 |
529 | // Should not duplicate common entries
530 | const nodeModulesMatches = content.match(/node_modules\//g);
531 | expect(nodeModulesMatches).toHaveLength(1);
532 |
533 | const logsMatches = content.match(/# Logs/g);
534 | expect(logsMatches).toHaveLength(1);
535 | });
536 |
537 | test('should handle project with existing task files in git', () => {
538 | const existingContent = `# Dependencies
539 | node_modules/
540 |
541 | # Logs
542 | *.log
543 |
544 | # Current task setup - keeping in git
545 | # Task files
546 | tasks.json
547 | tasks/
548 |
549 | # Build output
550 | dist/`;
551 |
552 | const templateContent = `# New template
553 | # Dependencies
554 | node_modules/
555 |
556 | # Task files
557 | tasks.json
558 | tasks/ `;
559 |
560 | fs.writeFileSync(testGitignorePath, existingContent);
561 |
562 | // Change preference to exclude tasks from git (storeTasksInGit = false means uncommented/ignored)
563 | manageGitignoreFile(testGitignorePath, templateContent, false);
564 |
565 | const content = fs.readFileSync(testGitignorePath, 'utf8');
566 |
567 | // Should retain existing content
568 | expect(content).toContain('# Dependencies');
569 | expect(content).toContain('node_modules/');
570 | expect(content).toContain('# Logs');
571 | expect(content).toContain('*.log');
572 | expect(content).toContain('# Build output');
573 | expect(content).toContain('dist/');
574 |
575 | // Should update task preference to uncommented (storeTasksInGit = false)
576 | expect(content).toMatch(
577 | /# Task files\s*[\r\n]+tasks\.json\s*[\r\n]+tasks\/ /
578 | );
579 | });
580 | });
581 | });
582 |
```
--------------------------------------------------------------------------------
/tests/unit/scripts/modules/ui/cross-tag-error-display.test.js:
--------------------------------------------------------------------------------
```javascript
1 | import { jest } from '@jest/globals';
2 | import {
3 | displayCrossTagDependencyError,
4 | displaySubtaskMoveError,
5 | displayInvalidTagCombinationError,
6 | displayDependencyValidationHints,
7 | formatTaskIdForDisplay
8 | } from '../../../../../scripts/modules/ui.js';
9 |
10 | // Mock console.log to capture output
11 | const originalConsoleLog = console.log;
12 | const mockConsoleLog = jest.fn();
13 | global.console.log = mockConsoleLog;
14 |
15 | // Add afterAll hook to restore
16 | afterAll(() => {
17 | global.console.log = originalConsoleLog;
18 | });
19 |
20 | describe('Cross-Tag Error Display Functions', () => {
21 | beforeEach(() => {
22 | mockConsoleLog.mockClear();
23 | });
24 |
25 | describe('displayCrossTagDependencyError', () => {
26 | it('should display cross-tag dependency error with conflicts', () => {
27 | const conflicts = [
28 | {
29 | taskId: 1,
30 | dependencyId: 2,
31 | dependencyTag: 'backlog',
32 | message: 'Task 1 depends on 2 (in backlog)'
33 | },
34 | {
35 | taskId: 3,
36 | dependencyId: 4,
37 | dependencyTag: 'done',
38 | message: 'Task 3 depends on 4 (in done)'
39 | }
40 | ];
41 |
42 | displayCrossTagDependencyError(conflicts, 'in-progress', 'done', '1,3');
43 |
44 | expect(mockConsoleLog).toHaveBeenCalledWith(
45 | expect.stringContaining(
46 | '❌ Cannot move tasks from "in-progress" to "done"'
47 | )
48 | );
49 | expect(mockConsoleLog).toHaveBeenCalledWith(
50 | expect.stringContaining('Cross-tag dependency conflicts detected:')
51 | );
52 | expect(mockConsoleLog).toHaveBeenCalledWith(
53 | expect.stringContaining('• Task 1 depends on 2 (in backlog)')
54 | );
55 | expect(mockConsoleLog).toHaveBeenCalledWith(
56 | expect.stringContaining('• Task 3 depends on 4 (in done)')
57 | );
58 | expect(mockConsoleLog).toHaveBeenCalledWith(
59 | expect.stringContaining('Resolution options:')
60 | );
61 | expect(mockConsoleLog).toHaveBeenCalledWith(
62 | expect.stringContaining('--with-dependencies')
63 | );
64 | expect(mockConsoleLog).toHaveBeenCalledWith(
65 | expect.stringContaining('--ignore-dependencies')
66 | );
67 | });
68 |
69 | it('should handle empty conflicts array', () => {
70 | displayCrossTagDependencyError([], 'backlog', 'done', '1');
71 |
72 | expect(mockConsoleLog).toHaveBeenCalledWith(
73 | expect.stringContaining('❌ Cannot move tasks from "backlog" to "done"')
74 | );
75 | expect(mockConsoleLog).toHaveBeenCalledWith(
76 | expect.stringContaining('Cross-tag dependency conflicts detected:')
77 | );
78 | });
79 | });
80 |
81 | describe('displaySubtaskMoveError', () => {
82 | it('should display subtask movement restriction error', () => {
83 | displaySubtaskMoveError('5.2', 'backlog', 'in-progress');
84 |
85 | expect(mockConsoleLog).toHaveBeenCalledWith(
86 | expect.stringContaining(
87 | '❌ Cannot move subtask 5.2 directly between tags'
88 | )
89 | );
90 | expect(mockConsoleLog).toHaveBeenCalledWith(
91 | expect.stringContaining('Subtask movement restriction:')
92 | );
93 | expect(mockConsoleLog).toHaveBeenCalledWith(
94 | expect.stringContaining(
95 | '• Subtasks cannot be moved directly between tags'
96 | )
97 | );
98 | expect(mockConsoleLog).toHaveBeenCalledWith(
99 | expect.stringContaining('Resolution options:')
100 | );
101 | expect(mockConsoleLog).toHaveBeenCalledWith(
102 | expect.stringContaining('remove-subtask --id=5.2 --convert')
103 | );
104 | });
105 |
106 | it('should handle nested subtask IDs (three levels)', () => {
107 | displaySubtaskMoveError('5.2.1', 'feature-auth', 'production');
108 |
109 | expect(mockConsoleLog).toHaveBeenCalledWith(
110 | expect.stringContaining(
111 | '❌ Cannot move subtask 5.2.1 directly between tags'
112 | )
113 | );
114 | expect(mockConsoleLog).toHaveBeenCalledWith(
115 | expect.stringContaining('remove-subtask --id=5.2.1 --convert')
116 | );
117 | });
118 |
119 | it('should handle deeply nested subtask IDs (four levels)', () => {
120 | displaySubtaskMoveError('10.3.2.1', 'development', 'testing');
121 |
122 | expect(mockConsoleLog).toHaveBeenCalledWith(
123 | expect.stringContaining(
124 | '❌ Cannot move subtask 10.3.2.1 directly between tags'
125 | )
126 | );
127 | expect(mockConsoleLog).toHaveBeenCalledWith(
128 | expect.stringContaining('remove-subtask --id=10.3.2.1 --convert')
129 | );
130 | });
131 |
132 | it('should handle single-level subtask IDs', () => {
133 | displaySubtaskMoveError('15.1', 'master', 'feature-branch');
134 |
135 | expect(mockConsoleLog).toHaveBeenCalledWith(
136 | expect.stringContaining(
137 | '❌ Cannot move subtask 15.1 directly between tags'
138 | )
139 | );
140 | expect(mockConsoleLog).toHaveBeenCalledWith(
141 | expect.stringContaining('remove-subtask --id=15.1 --convert')
142 | );
143 | });
144 |
145 | it('should handle invalid subtask ID format gracefully', () => {
146 | displaySubtaskMoveError('invalid-id', 'tag1', 'tag2');
147 |
148 | expect(mockConsoleLog).toHaveBeenCalledWith(
149 | expect.stringContaining(
150 | '❌ Cannot move subtask invalid-id directly between tags'
151 | )
152 | );
153 | expect(mockConsoleLog).toHaveBeenCalledWith(
154 | expect.stringContaining('remove-subtask --id=invalid-id --convert')
155 | );
156 | });
157 |
158 | it('should handle empty subtask ID', () => {
159 | displaySubtaskMoveError('', 'source', 'target');
160 |
161 | expect(mockConsoleLog).toHaveBeenCalledWith(
162 | expect.stringContaining(
163 | `❌ Cannot move subtask ${formatTaskIdForDisplay('')} directly between tags`
164 | )
165 | );
166 | expect(mockConsoleLog).toHaveBeenCalledWith(
167 | expect.stringContaining(
168 | `remove-subtask --id=${formatTaskIdForDisplay('')} --convert`
169 | )
170 | );
171 | });
172 |
173 | it('should handle null subtask ID', () => {
174 | displaySubtaskMoveError(null, 'source', 'target');
175 |
176 | expect(mockConsoleLog).toHaveBeenCalledWith(
177 | expect.stringContaining(
178 | '❌ Cannot move subtask null directly between tags'
179 | )
180 | );
181 | expect(mockConsoleLog).toHaveBeenCalledWith(
182 | expect.stringContaining('remove-subtask --id=null --convert')
183 | );
184 | });
185 |
186 | it('should handle undefined subtask ID', () => {
187 | displaySubtaskMoveError(undefined, 'source', 'target');
188 |
189 | expect(mockConsoleLog).toHaveBeenCalledWith(
190 | expect.stringContaining(
191 | '❌ Cannot move subtask undefined directly between tags'
192 | )
193 | );
194 | expect(mockConsoleLog).toHaveBeenCalledWith(
195 | expect.stringContaining('remove-subtask --id=undefined --convert')
196 | );
197 | });
198 |
199 | it('should handle special characters in subtask ID', () => {
200 | displaySubtaskMoveError('5.2@test', 'dev', 'prod');
201 |
202 | expect(mockConsoleLog).toHaveBeenCalledWith(
203 | expect.stringContaining(
204 | '❌ Cannot move subtask 5.2@test directly between tags'
205 | )
206 | );
207 | expect(mockConsoleLog).toHaveBeenCalledWith(
208 | expect.stringContaining('remove-subtask --id=5.2@test --convert')
209 | );
210 | });
211 |
212 | it('should handle numeric subtask IDs', () => {
213 | displaySubtaskMoveError('123.456', 'alpha', 'beta');
214 |
215 | expect(mockConsoleLog).toHaveBeenCalledWith(
216 | expect.stringContaining(
217 | '❌ Cannot move subtask 123.456 directly between tags'
218 | )
219 | );
220 | expect(mockConsoleLog).toHaveBeenCalledWith(
221 | expect.stringContaining('remove-subtask --id=123.456 --convert')
222 | );
223 | });
224 |
225 | it('should handle identical source and target tags', () => {
226 | displaySubtaskMoveError('7.3', 'same-tag', 'same-tag');
227 |
228 | expect(mockConsoleLog).toHaveBeenCalledWith(
229 | expect.stringContaining(
230 | '❌ Cannot move subtask 7.3 directly between tags'
231 | )
232 | );
233 | expect(mockConsoleLog).toHaveBeenCalledWith(
234 | expect.stringContaining('• Source tag: "same-tag"')
235 | );
236 | expect(mockConsoleLog).toHaveBeenCalledWith(
237 | expect.stringContaining('• Target tag: "same-tag"')
238 | );
239 | });
240 |
241 | it('should handle empty tag names', () => {
242 | displaySubtaskMoveError('9.1', '', '');
243 |
244 | expect(mockConsoleLog).toHaveBeenCalledWith(
245 | expect.stringContaining(
246 | '❌ Cannot move subtask 9.1 directly between tags'
247 | )
248 | );
249 | expect(mockConsoleLog).toHaveBeenCalledWith(
250 | expect.stringContaining('• Source tag: ""')
251 | );
252 | expect(mockConsoleLog).toHaveBeenCalledWith(
253 | expect.stringContaining('• Target tag: ""')
254 | );
255 | });
256 |
257 | it('should handle null tag names', () => {
258 | displaySubtaskMoveError('12.4', null, null);
259 |
260 | expect(mockConsoleLog).toHaveBeenCalledWith(
261 | expect.stringContaining(
262 | '❌ Cannot move subtask 12.4 directly between tags'
263 | )
264 | );
265 | expect(mockConsoleLog).toHaveBeenCalledWith(
266 | expect.stringContaining('• Source tag: "null"')
267 | );
268 | expect(mockConsoleLog).toHaveBeenCalledWith(
269 | expect.stringContaining('• Target tag: "null"')
270 | );
271 | });
272 |
273 | it('should handle complex tag names with special characters', () => {
274 | displaySubtaskMoveError(
275 | '3.2.1',
276 | 'feature/[email protected]',
277 | 'production@stable'
278 | );
279 |
280 | expect(mockConsoleLog).toHaveBeenCalledWith(
281 | expect.stringContaining(
282 | '❌ Cannot move subtask 3.2.1 directly between tags'
283 | )
284 | );
285 | expect(mockConsoleLog).toHaveBeenCalledWith(
286 | expect.stringContaining('• Source tag: "feature/[email protected]"')
287 | );
288 | expect(mockConsoleLog).toHaveBeenCalledWith(
289 | expect.stringContaining('• Target tag: "production@stable"')
290 | );
291 | });
292 |
293 | it('should handle very long subtask IDs', () => {
294 | const longId = '1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20';
295 | displaySubtaskMoveError(longId, 'short', 'long');
296 |
297 | expect(mockConsoleLog).toHaveBeenCalledWith(
298 | expect.stringContaining(
299 | `❌ Cannot move subtask ${longId} directly between tags`
300 | )
301 | );
302 | expect(mockConsoleLog).toHaveBeenCalledWith(
303 | expect.stringContaining(`remove-subtask --id=${longId} --convert`)
304 | );
305 | });
306 |
307 | it('should handle whitespace in subtask ID', () => {
308 | displaySubtaskMoveError(' 5.2 ', 'clean', 'dirty');
309 |
310 | expect(mockConsoleLog).toHaveBeenCalledWith(
311 | expect.stringContaining(
312 | '❌ Cannot move subtask 5.2 directly between tags'
313 | )
314 | );
315 | expect(mockConsoleLog).toHaveBeenCalledWith(
316 | expect.stringContaining('remove-subtask --id= 5.2 --convert')
317 | );
318 | });
319 | });
320 |
321 | describe('displayInvalidTagCombinationError', () => {
322 | it('should display invalid tag combination error', () => {
323 | displayInvalidTagCombinationError(
324 | 'backlog',
325 | 'backlog',
326 | 'Source and target tags are identical'
327 | );
328 |
329 | expect(mockConsoleLog).toHaveBeenCalledWith(
330 | expect.stringContaining('❌ Invalid tag combination')
331 | );
332 | expect(mockConsoleLog).toHaveBeenCalledWith(
333 | expect.stringContaining('Error details:')
334 | );
335 | expect(mockConsoleLog).toHaveBeenCalledWith(
336 | expect.stringContaining('• Source tag: "backlog"')
337 | );
338 | expect(mockConsoleLog).toHaveBeenCalledWith(
339 | expect.stringContaining('• Target tag: "backlog"')
340 | );
341 | expect(mockConsoleLog).toHaveBeenCalledWith(
342 | expect.stringContaining(
343 | '• Reason: Source and target tags are identical'
344 | )
345 | );
346 | expect(mockConsoleLog).toHaveBeenCalledWith(
347 | expect.stringContaining('Resolution options:')
348 | );
349 | });
350 | });
351 |
352 | describe('displayDependencyValidationHints', () => {
353 | it('should display general hints by default', () => {
354 | displayDependencyValidationHints();
355 |
356 | expect(mockConsoleLog).toHaveBeenCalledWith(
357 | expect.stringContaining('Helpful hints:')
358 | );
359 | expect(mockConsoleLog).toHaveBeenCalledWith(
360 | expect.stringContaining('💡 Use "task-master validate-dependencies"')
361 | );
362 | expect(mockConsoleLog).toHaveBeenCalledWith(
363 | expect.stringContaining('💡 Use "task-master fix-dependencies"')
364 | );
365 | });
366 |
367 | it('should display before-move hints', () => {
368 | displayDependencyValidationHints('before-move');
369 |
370 | expect(mockConsoleLog).toHaveBeenCalledWith(
371 | expect.stringContaining('Helpful hints:')
372 | );
373 | expect(mockConsoleLog).toHaveBeenCalledWith(
374 | expect.stringContaining(
375 | '💡 Tip: Run "task-master validate-dependencies"'
376 | )
377 | );
378 | expect(mockConsoleLog).toHaveBeenCalledWith(
379 | expect.stringContaining('💡 Tip: Use "task-master fix-dependencies"')
380 | );
381 | });
382 |
383 | it('should display after-error hints', () => {
384 | displayDependencyValidationHints('after-error');
385 |
386 | expect(mockConsoleLog).toHaveBeenCalledWith(
387 | expect.stringContaining('Helpful hints:')
388 | );
389 | expect(mockConsoleLog).toHaveBeenCalledWith(
390 | expect.stringContaining(
391 | '🔧 Quick fix: Run "task-master validate-dependencies"'
392 | )
393 | );
394 | expect(mockConsoleLog).toHaveBeenCalledWith(
395 | expect.stringContaining(
396 | '🔧 Quick fix: Use "task-master fix-dependencies"'
397 | )
398 | );
399 | });
400 |
401 | it('should handle unknown context gracefully', () => {
402 | displayDependencyValidationHints('unknown-context');
403 |
404 | expect(mockConsoleLog).toHaveBeenCalledWith(
405 | expect.stringContaining('Helpful hints:')
406 | );
407 | // Should fall back to general hints
408 | expect(mockConsoleLog).toHaveBeenCalledWith(
409 | expect.stringContaining('💡 Use "task-master validate-dependencies"')
410 | );
411 | });
412 | });
413 | });
414 |
415 | /**
416 | * Test for ID type consistency in dependency comparisons
417 | * This test verifies that the fix for mixed string/number ID comparison issues works correctly
418 | */
419 |
420 | describe('ID Type Consistency in Dependency Comparisons', () => {
421 | test('should handle mixed string/number ID comparisons correctly', () => {
422 | // Test the pattern that was fixed in the move-task tests
423 | const sourceTasks = [
424 | { id: 1, title: 'Task 1' },
425 | { id: 2, title: 'Task 2' },
426 | { id: '3.1', title: 'Subtask 3.1' }
427 | ];
428 |
429 | const allTasks = [
430 | { id: 1, title: 'Task 1', dependencies: [2, '3.1'] },
431 | { id: 2, title: 'Task 2', dependencies: ['1'] },
432 | {
433 | id: 3,
434 | title: 'Task 3',
435 | subtasks: [{ id: 1, title: 'Subtask 3.1', dependencies: [1] }]
436 | }
437 | ];
438 |
439 | // Test the fixed pattern: normalize source IDs and compare with string conversion
440 | const sourceIds = sourceTasks.map((t) => t.id);
441 | const normalizedSourceIds = sourceIds.map((id) => String(id));
442 |
443 | // Test that dependencies are correctly identified regardless of type
444 | const result = [];
445 | allTasks.forEach((task) => {
446 | if (task.dependencies && Array.isArray(task.dependencies)) {
447 | const hasDependency = task.dependencies.some((depId) =>
448 | normalizedSourceIds.includes(String(depId))
449 | );
450 | if (hasDependency) {
451 | result.push(task.id);
452 | }
453 | }
454 | });
455 |
456 | // Verify that the comparison works correctly
457 | expect(result).toContain(1); // Task 1 has dependency on 2 and '3.1'
458 | expect(result).toContain(2); // Task 2 has dependency on '1'
459 |
460 | // Test edge cases
461 | const mixedDependencies = [
462 | { id: 1, dependencies: [1, 2, '3.1', '4.2'] },
463 | { id: 2, dependencies: ['1', 3, '5.1'] }
464 | ];
465 |
466 | const testSourceIds = [1, '3.1', 4];
467 | const normalizedTestSourceIds = testSourceIds.map((id) => String(id));
468 |
469 | mixedDependencies.forEach((task) => {
470 | const hasMatch = task.dependencies.some((depId) =>
471 | normalizedTestSourceIds.includes(String(depId))
472 | );
473 | expect(typeof hasMatch).toBe('boolean');
474 | expect(hasMatch).toBe(true); // Should find matches in both tasks
475 | });
476 | });
477 |
478 | test('should handle edge cases in ID normalization', () => {
479 | // Test various ID formats
480 | const testCases = [
481 | { source: 1, dependency: '1', expected: true },
482 | { source: '1', dependency: 1, expected: true },
483 | { source: '3.1', dependency: '3.1', expected: true },
484 | { source: 3, dependency: '3.1', expected: false }, // Different formats
485 | { source: '3.1', dependency: 3, expected: false }, // Different formats
486 | { source: 1, dependency: 2, expected: false }, // No match
487 | { source: '1.2', dependency: '1.2', expected: true },
488 | { source: 1, dependency: null, expected: false }, // Handle null
489 | { source: 1, dependency: undefined, expected: false } // Handle undefined
490 | ];
491 |
492 | testCases.forEach(({ source, dependency, expected }) => {
493 | const normalizedSourceIds = [String(source)];
494 | const hasMatch = normalizedSourceIds.includes(String(dependency));
495 | expect(hasMatch).toBe(expected);
496 | });
497 | });
498 | });
499 |
```