This is page 53 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
--------------------------------------------------------------------------------
/.taskmaster/docs/autonomous-tdd-git-workflow.md:
--------------------------------------------------------------------------------
```markdown
1 | ## Summary
2 |
3 | - Put the existing git and test workflows on rails: a repeatable, automated process that can run autonomously, with guardrails and a compact TUI for visibility.
4 |
5 | - Flow: for a selected task, create a branch named with the tag + task id → generate tests for the first subtask (red) using the Surgical Test Generator → implement code (green) → verify tests → commit → repeat per subtask → final verify → push → open PR against the default branch.
6 |
7 | - Build on existing rules: .cursor/rules/git_workflow.mdc, .cursor/rules/test_workflow.mdc, .claude/agents/surgical-test-generator.md, and existing CLI/core services.
8 |
9 | ## Goals
10 |
11 | - Deterministic, resumable automation to execute the TDD loop per subtask with minimal human intervention.
12 |
13 | - Strong guardrails: never commit to the default branch; only commit when tests pass; enforce status transitions; persist logs/state for debuggability.
14 |
15 | - Visibility: a compact terminal UI (like lazygit) to pick tag, view tasks, and start work; right-side pane opens an executor terminal (via tmux) for agent coding.
16 |
17 | - Extensible: framework-agnostic test generation via the Surgical Test Generator; detect and use the repo’s test command for execution with coverage thresholds.
18 |
19 | ## Non‑Goals (initial)
20 |
21 | - Full multi-language runner parity beyond detection and executing the project’s test command.
22 |
23 | - Complex GUI; start with CLI/TUI + tmux pane. IDE/extension can hook into the same state later.
24 |
25 | - Rich executor selection UX (codex/gemini/claude) — we’ll prompt per run; defaults can come later.
26 |
27 | ## Success Criteria
28 |
29 | - One command can autonomously complete a task's subtasks via TDD and open a PR when done.
30 |
31 | - All commits made on a branch that includes the tag and task id (see Branch Naming); no commits to the default branch directly.
32 |
33 | - Every subtask iteration: failing tests added first (red), then code added to pass them (green), commit only after green.
34 |
35 | - End-to-end logs + artifacts stored in .taskmaster/reports/runs/<timestamp-or-id>/.
36 |
37 | ## Success Metrics (Phase 1)
38 |
39 | - **Adoption**: 80% of tasks in a pilot repo completed via `tm autopilot`
40 | - **Safety**: 0 commits to default branch; 100% of commits have green tests
41 | - **Efficiency**: Average time from task start to PR < 30min for simple subtasks
42 | - **Reliability**: < 5% of runs require manual intervention (timeout/conflicts)
43 |
44 | ## User Stories
45 |
46 | - As a developer, I can run tm autopilot <taskId> and watch a structured, safe workflow execute.
47 |
48 | - As a reviewer, I can inspect commits per subtask, and a PR summarizing the work when the task completes.
49 |
50 | - As an operator, I can see current step, active subtask, tests status, and logs in a compact CLI view and read a final run report.
51 |
52 | ## Example Workflow Traces
53 |
54 | ### Happy Path: Complete a 3-subtask feature
55 |
56 | ```bash
57 | # Developer starts
58 | $ tm autopilot 42
59 | → Checks preflight: ✓ clean tree, ✓ npm test detected
60 | → Creates branch: analytics/task-42-user-metrics
61 | → Subtask 42.1: "Add metrics schema"
62 | RED: generates test_metrics_schema.test.js → 3 failures
63 | GREEN: implements schema.js → all pass
64 | COMMIT: "feat(metrics): add metrics schema (task 42.1)"
65 | → Subtask 42.2: "Add collection endpoint"
66 | RED: generates test_metrics_endpoint.test.js → 5 failures
67 | GREEN: implements api/metrics.js → all pass
68 | COMMIT: "feat(metrics): add collection endpoint (task 42.2)"
69 | → Subtask 42.3: "Add dashboard widget"
70 | RED: generates test_metrics_widget.test.js → 4 failures
71 | GREEN: implements components/MetricsWidget.jsx → all pass
72 | COMMIT: "feat(metrics): add dashboard widget (task 42.3)"
73 | → Final: all 3 subtasks complete
74 | ✓ Run full test suite → all pass
75 | ✓ Coverage check → 85% (meets 80% threshold)
76 | PUSH: confirms with user → pushed to origin
77 | PR: opens #123 "Task #42 [analytics]: User metrics tracking"
78 |
79 | ✓ Task 42 complete. PR: https://github.com/org/repo/pull/123
80 | Run report: .taskmaster/reports/runs/2025-01-15-142033/
81 | ```
82 |
83 | ### Error Recovery: Failing tests timeout
84 |
85 | ```bash
86 | $ tm autopilot 42
87 | → Subtask 42.2 GREEN phase: attempt 1 fails (2 tests still red)
88 | → Subtask 42.2 GREEN phase: attempt 2 fails (1 test still red)
89 | → Subtask 42.2 GREEN phase: attempt 3 fails (1 test still red)
90 |
91 | ⚠️ Paused: Could not achieve green state after 3 attempts
92 | 📋 State saved to: .taskmaster/reports/runs/2025-01-15-142033/
93 | Last error: "POST /api/metrics returns 500 instead of 201"
94 |
95 | Next steps:
96 | - Review diff: git diff HEAD
97 | - Inspect logs: cat .taskmaster/reports/runs/2025-01-15-142033/log.jsonl
98 | - Check test output: cat .taskmaster/reports/runs/2025-01-15-142033/test-results/subtask-42.2-green-attempt3.json
99 | - Resume after manual fix: tm autopilot --resume
100 |
101 | # Developer manually fixes the issue, then:
102 | $ tm autopilot --resume
103 | → Resuming subtask 42.2 GREEN phase
104 | GREEN: all tests pass
105 | COMMIT: "feat(metrics): add collection endpoint (task 42.2)"
106 | → Continuing to subtask 42.3...
107 | ```
108 |
109 | ### Dry Run: Preview before execution
110 |
111 | ```bash
112 | $ tm autopilot 42 --dry-run
113 | Autopilot Plan for Task #42 [analytics]: User metrics tracking
114 | ─────────────────────────────────────────────────────────────
115 | Preflight:
116 | ✓ Working tree is clean
117 | ✓ Test command detected: npm test
118 | ✓ Tools available: git, gh, node, npm
119 | ✓ Current branch: main (will create new branch)
120 |
121 | Branch & Tag:
122 | → Create branch: analytics/task-42-user-metrics
123 | → Set active tag: analytics
124 |
125 | Subtasks (3 pending):
126 | 1. 42.1: Add metrics schema
127 | - RED: generate tests in src/__tests__/schema.test.js
128 | - GREEN: implement src/schema.js
129 | - COMMIT: "feat(metrics): add metrics schema (task 42.1)"
130 |
131 | 2. 42.2: Add collection endpoint [depends on 42.1]
132 | - RED: generate tests in src/api/__tests__/metrics.test.js
133 | - GREEN: implement src/api/metrics.js
134 | - COMMIT: "feat(metrics): add collection endpoint (task 42.2)"
135 |
136 | 3. 42.3: Add dashboard widget [depends on 42.2]
137 | - RED: generate tests in src/components/__tests__/MetricsWidget.test.jsx
138 | - GREEN: implement src/components/MetricsWidget.jsx
139 | - COMMIT: "feat(metrics): add dashboard widget (task 42.3)"
140 |
141 | Finalization:
142 | → Run full test suite with coverage
143 | → Push branch to origin (will confirm)
144 | → Create PR targeting main
145 |
146 | Run without --dry-run to execute.
147 | ```
148 |
149 | ## High‑Level Workflow
150 |
151 | 1) Pre‑flight
152 |
153 | - Verify clean working tree or confirm staging/commit policy (configurable).
154 |
155 | - Detect repo type and the project’s test command (e.g., npm test, pnpm test, pytest, go test).
156 |
157 | - Validate tools: git, gh (optional for PR), node/npm, and (if used) claude CLI.
158 |
159 | - Load TaskMaster state and selected task; if no subtasks exist, automatically run “expand” before working.
160 |
161 | 2) Branch & Tag Setup
162 |
163 | - Checkout default branch and update (optional), then create a branch using Branch Naming (below).
164 |
165 | - Map branch ↔ tag via existing tag management; explicitly set active tag to the branch’s tag.
166 |
167 | 3) Subtask Loop (for each pending/in-progress subtask in dependency order)
168 |
169 | - Select next eligible subtask using tm-core TaskService getNextTask() and subtask eligibility logic.
170 |
171 | - Red: generate or update failing tests for the subtask
172 |
173 | - Use the Surgical Test Generator system prompt .claude/agents/surgical-test-generator.md) to produce high-signal tests following project conventions.
174 |
175 | - Run tests to confirm red; record results. If not red (already passing), skip to next subtask or escalate.
176 |
177 | - Green: implement code to pass tests
178 |
179 | - Use executor to implement changes (initial: claude CLI prompt with focused context).
180 |
181 | - Re-run tests until green or timeout/backoff policy triggers.
182 |
183 | - Commit: when green
184 |
185 | - Commit tests + code with conventional commit message. Optionally update subtask status to done.
186 |
187 | - Persist run step metadata/logs.
188 |
189 | 4) Finalization
190 |
191 | - Run full test suite and coverage (if configured); optionally lint/format.
192 |
193 | - Commit any final adjustments.
194 |
195 | - Push branch (ask user to confirm); create PR (via gh pr create) targeting the default branch. Title format: Task #<id> [<tag>]: <title>.
196 |
197 | 5) Post‑Run
198 |
199 | - Update task status if desired (e.g., review).
200 |
201 | - Persist run report (JSON + markdown summary) to .taskmaster/reports/runs/<run-id>/.
202 |
203 | ## Guardrails
204 |
205 | - Never commit to the default branch.
206 |
207 | - Commit only if all tests (targeted and suite) pass; allow override flags.
208 |
209 | - Enforce 80% coverage thresholds (lines/branches/functions/statements) by default; configurable.
210 |
211 | - Timebox/model ops and retries; if not green within N attempts, pause with actionable state for resume.
212 |
213 | - Always log actions, commands, and outcomes; include dry-run mode.
214 |
215 | - Ask before branch creation, pushing, and opening a PR unless --no-confirm is set.
216 |
217 | ## Integration Points (Current Repo)
218 |
219 | - CLI: apps/cli provides command structure and UI components.
220 |
221 | - New command: tm autopilot (alias: task-master autopilot).
222 |
223 | - Reuse UI components under apps/cli/src/ui/components/ for headers/task details/next-task.
224 |
225 | - Core services: packages/tm-core
226 |
227 | - TaskService for selection, status, tags.
228 |
229 | - TaskExecutionService for prompt formatting and executor prep.
230 |
231 | - Executors: claude executor and ExecutorFactory to run external tools.
232 |
233 | - Proposed new: WorkflowOrchestrator to drive the autonomous loop and emit progress events.
234 |
235 | - Tag/Git utilities: scripts/modules/utils/git-utils.js and scripts/modules/task-manager/tag-management.js for branch→tag mapping and explicit tag switching.
236 |
237 | - Rules: .cursor/rules/git_workflow.mdc and .cursor/rules/test_workflow.mdc to steer behavior and ensure consistency.
238 |
239 | - Test generation prompt: .claude/agents/surgical-test-generator.md.
240 |
241 | ## Proposed Components
242 |
243 | - Orchestrator (tm-core): WorkflowOrchestrator (new)
244 |
245 | - State machine driving phases: Preflight → Branch/Tag → SubtaskIter (Red/Green/Commit) → Finalize → PR.
246 |
247 | - Exposes an evented API (progress events) that the CLI can render.
248 |
249 | - Stores run state artifacts.
250 |
251 | - Test Runner Adapter
252 |
253 | - Detects and runs tests via the project’s test command (e.g., npm test), with targeted runs where feasible.
254 |
255 | - API: runTargeted(files/pattern), runAll(), report summary (failures, duration, coverage), enforce 80% threshold by default.
256 |
257 | - Git/PR Adapter
258 |
259 | - Encapsulates git ops: branch create/checkout, add/commit, push.
260 |
261 | - Optional gh integration to open PR; fallback to instructions if gh unavailable.
262 |
263 | - Confirmation gates for branch creation and pushes.
264 |
265 | - Prompt/Exec Adapter
266 |
267 | - Uses existing executor service to call the selected coding assistant (initially claude) with tight prompts: task/subtask context, surgical tests first, then minimal code to green.
268 |
269 | - Run State + Reporting
270 |
271 | - JSONL log of steps, timestamps, commands, test results.
272 |
273 | - Markdown summary for PR description and post-run artifact.
274 |
275 | ## CLI UX (MVP)
276 |
277 | - Command: tm autopilot [taskId]
278 |
279 | - Flags: --dry-run, --no-push, --no-pr, --no-confirm, --force, --max-attempts <n>, --runner <auto|custom>, --commit-scope <scope>
280 |
281 | - Output: compact header (project, tag, branch), current phase, subtask line, last test summary, next actions.
282 |
283 | - Resume: If interrupted, tm autopilot --resume picks up from last checkpoint in run state.
284 |
285 | ### TUI with tmux (Linear Execution)
286 |
287 | - Left pane: Tag selector, task list (status/priority), start/expand shortcuts; "Start" triggers the next task or a selected task.
288 |
289 | - Right pane: Executor terminal (tmux split) that runs the coding agent (claude-code/codex). Autopilot can hand over to the right pane during green.
290 |
291 | - MCP integration: use MCP tools for task queries/updates and for shell/test invocations where available.
292 |
293 | ## TUI Layout (tmux-based)
294 |
295 | ### Pane Structure
296 |
297 | ```
298 | ┌─────────────────────────────────────┬──────────────────────────────────┐
299 | │ Task Navigator (left) │ Executor Terminal (right) │
300 | │ │ │
301 | │ Project: my-app │ $ tm autopilot --executor-mode │
302 | │ Branch: analytics/task-42 │ > Running subtask 42.2 GREEN... │
303 | │ Tag: analytics │ > Implementing endpoint... │
304 | │ │ > Tests: 3 passed, 0 failed │
305 | │ Tasks: │ > Ready to commit │
306 | │ → 42 [in-progress] User metrics │ │
307 | │ → 42.1 [done] Schema │ [Live output from Claude Code] │
308 | │ → 42.2 [active] Endpoint ◀ │ │
309 | │ → 42.3 [pending] Dashboard │ │
310 | │ │ │
311 | │ [s] start [p] pause [q] quit │ │
312 | └─────────────────────────────────────┴──────────────────────────────────┘
313 | ```
314 |
315 | ### Implementation Notes
316 |
317 | - **Left pane**: `apps/cli/src/ui/tui/navigator.ts` (new, uses `blessed` or `ink`)
318 | - **Right pane**: spawned via `tmux split-window -h` running `tm autopilot --executor-mode`
319 | - **Communication**: shared state file `.taskmaster/state/current-run.json` + file watching or event stream
320 | - **Keybindings**:
321 | - `s` - Start selected task
322 | - `p` - Pause/resume current run
323 | - `q` - Quit (with confirmation if run active)
324 | - `↑/↓` - Navigate task list
325 | - `Enter` - Expand/collapse subtasks
326 |
327 | ## Prompt Composition (Detailed)
328 |
329 | ### System Prompt Assembly
330 |
331 | Prompts are composed in three layers:
332 |
333 | 1. **Base rules** (loaded in order from `.cursor/rules/` and `.claude/agents/`):
334 | - `git_workflow.mdc` → git commit conventions, branch policy, PR guidelines
335 | - `test_workflow.mdc` → TDD loop requirements, coverage thresholds, test structure
336 | - `surgical-test-generator.md` → test generation methodology, project-specific test patterns
337 |
338 | 2. **Task context injection**:
339 | ```
340 | You are implementing:
341 | Task #42 [analytics]: User metrics tracking
342 | Subtask 42.2: Add collection endpoint
343 |
344 | Description:
345 | Implement POST /api/metrics endpoint to collect user metrics events
346 |
347 | Acceptance criteria:
348 | - POST /api/metrics accepts { userId, eventType, timestamp }
349 | - Validates input schema (reject missing/invalid fields)
350 | - Persists to database
351 | - Returns 201 on success with created record
352 | - Returns 400 on validation errors
353 |
354 | Dependencies:
355 | - Subtask 42.1 (metrics schema) is complete
356 |
357 | Current phase: RED (generate failing tests)
358 | Test command: npm test
359 | Test file convention: src/**/*.test.js (vitest framework detected)
360 | Branch: analytics/task-42-user-metrics
361 | Project language: JavaScript (Node.js)
362 | ```
363 |
364 | 3. **Phase-specific instructions**:
365 | - **RED phase**: "Generate minimal failing tests for this subtask. Do NOT implement any production code. Only create test files. Confirm tests fail with clear error messages indicating missing implementation."
366 | - **GREEN phase**: "Implement minimal code to pass the failing tests. Follow existing project patterns in `src/`. Only modify files necessary for this subtask. Keep changes focused and reviewable."
367 |
368 | ### Example Full Prompt (RED Phase)
369 |
370 | ```markdown
371 | <SYSTEM PROMPT>
372 | [Contents of .cursor/rules/git_workflow.mdc]
373 | [Contents of .cursor/rules/test_workflow.mdc]
374 | [Contents of .claude/agents/surgical-test-generator.md]
375 |
376 | <TASK CONTEXT>
377 | You are implementing:
378 | Task #42.2: Add collection endpoint
379 |
380 | Description:
381 | Implement POST /api/metrics endpoint to collect user metrics events
382 |
383 | Acceptance criteria:
384 | - POST /api/metrics accepts { userId, eventType, timestamp }
385 | - Validates input schema (reject missing/invalid fields)
386 | - Persists to database using MetricsSchema from subtask 42.1
387 | - Returns 201 on success with created record
388 | - Returns 400 on validation errors with details
389 |
390 | Dependencies: Subtask 42.1 (metrics schema) is complete
391 |
392 | <INSTRUCTION>
393 | Generate failing tests for this subtask. Follow project conventions:
394 | - Test file: src/api/__tests__/metrics.test.js
395 | - Framework: vitest (detected from package.json)
396 | - Test cases to cover:
397 | * POST /api/metrics with valid payload → should return 201 (will fail: endpoint not implemented)
398 | * POST /api/metrics with missing userId → should return 400 (will fail: validation not implemented)
399 | * POST /api/metrics with invalid timestamp → should return 400 (will fail: validation not implemented)
400 | * POST /api/metrics should persist to database → should save record (will fail: persistence not implemented)
401 |
402 | Do NOT implement the endpoint code yet. Only create test file(s).
403 | Confirm tests fail with messages like "Cannot POST /api/metrics" or "endpoint not defined".
404 |
405 | Output format:
406 | 1. File path to create: src/api/__tests__/metrics.test.js
407 | 2. Complete test code
408 | 3. Command to run: npm test src/api/__tests__/metrics.test.js
409 | ```
410 |
411 | ### Example Full Prompt (GREEN Phase)
412 |
413 | ```markdown
414 | <SYSTEM PROMPT>
415 | [Contents of .cursor/rules/git_workflow.mdc]
416 | [Contents of .cursor/rules/test_workflow.mdc]
417 |
418 | <TASK CONTEXT>
419 | Task #42.2: Add collection endpoint
420 | [same context as RED phase]
421 |
422 | <CURRENT STATE>
423 | Tests created in RED phase:
424 | - src/api/__tests__/metrics.test.js
425 | - 5 tests written, all failing as expected
426 |
427 | Test output:
428 | ```
429 | FAIL src/api/__tests__/metrics.test.js
430 | POST /api/metrics
431 | ✗ should return 201 with valid payload (endpoint not found)
432 | ✗ should return 400 with missing userId (endpoint not found)
433 | ✗ should return 400 with invalid timestamp (endpoint not found)
434 | ✗ should persist to database (endpoint not found)
435 | ```
436 |
437 | <INSTRUCTION>
438 | Implement minimal code to make all tests pass.
439 |
440 | Guidelines:
441 | - Create/modify file: src/api/metrics.js
442 | - Use existing patterns from src/api/ (e.g., src/api/users.js for reference)
443 | - Import MetricsSchema from subtask 42.1 (src/models/schema.js)
444 | - Implement validation, persistence, and response handling
445 | - Follow project error handling conventions
446 | - Keep implementation focused on this subtask only
447 |
448 | After implementation:
449 | 1. Run tests: npm test src/api/__tests__/metrics.test.js
450 | 2. Confirm all 5 tests pass
451 | 3. Report results
452 |
453 | Output format:
454 | 1. File(s) created/modified
455 | 2. Implementation code
456 | 3. Test command and results
457 | ```
458 |
459 | ### Prompt Loading Configuration
460 |
461 | See `.taskmaster/config.json` → `prompts` section for paths and load order.
462 |
463 | ## Configuration Schema
464 |
465 | ### .taskmaster/config.json
466 |
467 | ```json
468 | {
469 | "autopilot": {
470 | "enabled": true,
471 | "requireCleanWorkingTree": true,
472 | "commitTemplate": "{type}({scope}): {msg}",
473 | "defaultCommitType": "feat",
474 | "maxGreenAttempts": 3,
475 | "testTimeout": 300000
476 | },
477 | "test": {
478 | "runner": "auto",
479 | "coverageThresholds": {
480 | "lines": 80,
481 | "branches": 80,
482 | "functions": 80,
483 | "statements": 80
484 | },
485 | "targetedRunPattern": "**/*.test.js"
486 | },
487 | "git": {
488 | "branchPattern": "{tag}/task-{id}-{slug}",
489 | "pr": {
490 | "enabled": true,
491 | "base": "default",
492 | "bodyTemplate": ".taskmaster/templates/pr-body.md"
493 | }
494 | },
495 | "prompts": {
496 | "rulesPath": ".cursor/rules",
497 | "testGeneratorPath": ".claude/agents/surgical-test-generator.md",
498 | "loadOrder": ["git_workflow.mdc", "test_workflow.mdc"]
499 | }
500 | }
501 | ```
502 |
503 | ### Configuration Fields
504 |
505 | #### autopilot
506 | - `enabled` (boolean): Enable/disable autopilot functionality
507 | - `requireCleanWorkingTree` (boolean): Require clean git state before starting
508 | - `commitTemplate` (string): Template for commit messages (tokens: `{type}`, `{scope}`, `{msg}`)
509 | - `defaultCommitType` (string): Default commit type (feat, fix, chore, etc.)
510 | - `maxGreenAttempts` (number): Maximum retry attempts to achieve green tests (default: 3)
511 | - `testTimeout` (number): Timeout in milliseconds per test run (default: 300000 = 5min)
512 |
513 | #### test
514 | - `runner` (string): Test runner detection mode (`"auto"` or explicit command like `"npm test"`)
515 | - `coverageThresholds` (object): Minimum coverage percentages required
516 | - `lines`, `branches`, `functions`, `statements` (number): Threshold percentages (0-100)
517 | - `targetedRunPattern` (string): Glob pattern for targeted subtask test runs
518 |
519 | #### git
520 | - `branchPattern` (string): Branch naming pattern (tokens: `{tag}`, `{id}`, `{slug}`)
521 | - `pr.enabled` (boolean): Enable automatic PR creation
522 | - `pr.base` (string): Target branch for PRs (`"default"` uses repo default, or specify like `"main"`)
523 | - `pr.bodyTemplate` (string): Path to PR body template file (optional)
524 |
525 | #### prompts
526 | - `rulesPath` (string): Directory containing rule files (e.g., `.cursor/rules`)
527 | - `testGeneratorPath` (string): Path to test generator prompt file
528 | - `loadOrder` (array): Order to load rule files from `rulesPath`
529 |
530 | ### Environment Variables
531 |
532 | ```bash
533 | # Required for executor
534 | ANTHROPIC_API_KEY=sk-ant-... # Claude API key
535 |
536 | # Optional: for PR creation
537 | GITHUB_TOKEN=ghp_... # GitHub personal access token
538 |
539 | # Optional: for other executors (future)
540 | OPENAI_API_KEY=sk-...
541 | GOOGLE_API_KEY=...
542 | ```
543 |
544 | ## Run Artifacts & Observability
545 |
546 | ### Per-Run Artifact Structure
547 |
548 | Each autopilot run creates a timestamped directory with complete traceability:
549 |
550 | ```
551 | .taskmaster/reports/runs/2025-01-15-142033/
552 | ├── manifest.json # run metadata (task id, start/end time, status)
553 | ├── log.jsonl # timestamped event stream
554 | ├── commits.txt # list of commit SHAs made during run
555 | ├── test-results/
556 | │ ├── subtask-42.1-red.json
557 | │ ├── subtask-42.1-green.json
558 | │ ├── subtask-42.2-red.json
559 | │ ├── subtask-42.2-green-attempt1.json
560 | │ ├── subtask-42.2-green-attempt2.json
561 | │ ├── subtask-42.2-green-attempt3.json
562 | │ └── final-suite.json
563 | └── pr.md # generated PR body
564 | ```
565 |
566 | ### manifest.json Format
567 |
568 | ```json
569 | {
570 | "runId": "2025-01-15-142033",
571 | "taskId": "42",
572 | "tag": "analytics",
573 | "branch": "analytics/task-42-user-metrics",
574 | "startTime": "2025-01-15T14:20:33Z",
575 | "endTime": "2025-01-15T14:45:12Z",
576 | "status": "completed",
577 | "subtasksCompleted": ["42.1", "42.2", "42.3"],
578 | "subtasksFailed": [],
579 | "totalCommits": 3,
580 | "prUrl": "https://github.com/org/repo/pull/123",
581 | "finalCoverage": {
582 | "lines": 85.3,
583 | "branches": 82.1,
584 | "functions": 88.9,
585 | "statements": 85.0
586 | }
587 | }
588 | ```
589 |
590 | ### log.jsonl Format
591 |
592 | Event stream in JSON Lines format for easy parsing and debugging:
593 |
594 | ```jsonl
595 | {"ts":"2025-01-15T14:20:33Z","phase":"preflight","status":"ok","details":{"testCmd":"npm test","gitClean":true}}
596 | {"ts":"2025-01-15T14:20:45Z","phase":"branch","status":"ok","branch":"analytics/task-42-user-metrics"}
597 | {"ts":"2025-01-15T14:21:00Z","phase":"red","subtask":"42.1","status":"ok","tests":{"failed":3,"passed":0}}
598 | {"ts":"2025-01-15T14:22:15Z","phase":"green","subtask":"42.1","status":"ok","tests":{"passed":3,"failed":0},"attempts":2}
599 | {"ts":"2025-01-15T14:22:20Z","phase":"commit","subtask":"42.1","status":"ok","sha":"a1b2c3d","message":"feat(metrics): add metrics schema (task 42.1)"}
600 | {"ts":"2025-01-15T14:23:00Z","phase":"red","subtask":"42.2","status":"ok","tests":{"failed":5,"passed":0}}
601 | {"ts":"2025-01-15T14:25:30Z","phase":"green","subtask":"42.2","status":"error","tests":{"passed":3,"failed":2},"attempts":3,"error":"Max attempts reached"}
602 | {"ts":"2025-01-15T14:25:35Z","phase":"pause","reason":"max_attempts","nextAction":"manual_review"}
603 | ```
604 |
605 | ### Test Results Format
606 |
607 | Each test run stores detailed results:
608 |
609 | ```json
610 | {
611 | "subtask": "42.2",
612 | "phase": "green",
613 | "attempt": 3,
614 | "timestamp": "2025-01-15T14:25:30Z",
615 | "command": "npm test src/api/__tests__/metrics.test.js",
616 | "exitCode": 1,
617 | "duration": 2340,
618 | "summary": {
619 | "total": 5,
620 | "passed": 3,
621 | "failed": 2,
622 | "skipped": 0
623 | },
624 | "failures": [
625 | {
626 | "test": "POST /api/metrics should return 201 with valid payload",
627 | "error": "Expected status 201, got 500",
628 | "stack": "..."
629 | }
630 | ],
631 | "coverage": {
632 | "lines": 78.5,
633 | "branches": 75.0,
634 | "functions": 80.0,
635 | "statements": 78.5
636 | }
637 | }
638 | ```
639 |
640 | ## Execution Model
641 |
642 | ### Orchestration vs Direct Execution
643 |
644 | The autopilot system uses an **orchestration model** rather than direct code execution:
645 |
646 | **Orchestrator Role** (tm-core WorkflowOrchestrator):
647 | - Maintains state machine tracking current phase (RED/GREEN/COMMIT) per subtask
648 | - Validates preconditions (tests pass, git state clean, etc.)
649 | - Returns "work units" describing what needs to be done next
650 | - Records completion and advances to next phase
651 | - Persists state for resumability
652 |
653 | **Executor Role** (Claude Code/AI session via MCP):
654 | - Queries orchestrator for next work unit
655 | - Executes the work (generates tests, writes code, runs tests, makes commits)
656 | - Reports results back to orchestrator
657 | - Handles file operations and tool invocations
658 |
659 | **Why This Approach?**
660 | - Leverages existing AI capabilities (Claude Code) rather than duplicating them
661 | - MCP protocol provides clean separation between state management and execution
662 | - Allows human oversight and intervention at each phase
663 | - Simpler to implement: orchestrator is pure state logic, no code generation needed
664 | - Enables multiple executor types (Claude Code, other AI tools, human developers)
665 |
666 | **Example Flow**:
667 | ```typescript
668 | // Claude Code (via MCP) queries orchestrator
669 | const workUnit = await orchestrator.getNextWorkUnit('42');
670 | // => {
671 | // phase: 'RED',
672 | // subtask: '42.1',
673 | // action: 'Generate failing tests for metrics schema',
674 | // context: { title, description, dependencies, testFile: 'src/__tests__/schema.test.js' }
675 | // }
676 |
677 | // Claude Code executes the work (writes test file, runs tests)
678 | // Then reports back
679 | await orchestrator.completeWorkUnit('42', '42.1', 'RED', {
680 | success: true,
681 | testsCreated: ['src/__tests__/schema.test.js'],
682 | testsFailed: 3
683 | });
684 |
685 | // Query again for next phase
686 | const nextWorkUnit = await orchestrator.getNextWorkUnit('42');
687 | // => { phase: 'GREEN', subtask: '42.1', action: 'Implement code to pass tests', ... }
688 | ```
689 |
690 | ## Design Decisions
691 |
692 | ### Why commit per subtask instead of per task?
693 |
694 | **Decision**: Commit after each subtask's green state, not after the entire task.
695 |
696 | **Rationale**:
697 | - Atomic commits make code review easier (reviewers can see logical progression)
698 | - Easier to revert a single subtask if it causes issues downstream
699 | - Matches the TDD loop's natural checkpoint and cognitive boundary
700 | - Provides resumability points if the run is interrupted
701 |
702 | **Trade-off**: More commits per task (can use squash-merge in PRs if desired)
703 |
704 | ### Why not support parallel subtask execution?
705 |
706 | **Decision**: Sequential subtask execution in Phase 1; parallel execution deferred to Phase 3.
707 |
708 | **Rationale**:
709 | - Subtasks often have implicit dependencies (e.g., schema before endpoint, endpoint before UI)
710 | - Simpler orchestrator state machine (less complexity = faster to ship)
711 | - Parallel execution requires explicit dependency DAG and conflict resolution
712 | - Can be added in Phase 3 once core workflow is proven stable
713 |
714 | **Trade-off**: Slower for truly independent subtasks (mitigated by keeping subtasks small and focused)
715 |
716 | ### Why require 80% coverage by default?
717 |
718 | **Decision**: Enforce 80% coverage threshold (lines/branches/functions/statements) before allowing commits.
719 |
720 | **Rationale**:
721 | - Industry standard baseline for production code quality
722 | - Forces test generation to be comprehensive, not superficial
723 | - Configurable per project via `.taskmaster/config.json` if too strict
724 | - Prevents "green tests" that only test happy paths
725 |
726 | **Trade-off**: May require more test generation iterations; can be lowered per project
727 |
728 | ### Why use tmux instead of a rich GUI?
729 |
730 | **Decision**: MVP uses tmux split panes for TUI, not Electron/web-based GUI.
731 |
732 | **Rationale**:
733 | - Tmux is universally available on dev machines; no installation burden
734 | - Terminal-first workflows match developer mental model (no context switching)
735 | - Simpler to implement and maintain; can add GUI later via extensions
736 | - State stored in files allows IDE/extension integration without coupling
737 |
738 | **Trade-off**: Less visual polish than GUI; requires tmux familiarity
739 |
740 | ### Why not support multiple executors (codex/gemini/claude) in Phase 1?
741 |
742 | **Decision**: Start with Claude executor only; add others in Phase 2+.
743 |
744 | **Rationale**:
745 | - Reduces scope and complexity for initial delivery
746 | - Claude Code already integrated with existing executor service
747 | - Executor abstraction already exists; adding more is straightforward later
748 | - Different executors may need different prompt strategies (requires experimentation)
749 |
750 | **Trade-off**: Users locked to Claude initially; can work around with manual executor selection
751 |
752 | ## Risks and Mitigations
753 |
754 | - Model hallucination/large diffs: restrict prompt scope; enforce minimal changes; show diff previews (optional) before commit.
755 |
756 | - Flaky tests: allow retries, isolate targeted runs for speed, then full suite before commit.
757 |
758 | - Environment variability: detect runners/tools; provide fallbacks and actionable errors.
759 |
760 | - PR creation fails: still push and print manual commands; persist PR body to reuse.
761 |
762 | ## Open Questions
763 |
764 | 1) Slugging rules for branch names; any length limits or normalization beyond {slug} token sanitize?
765 |
766 | 2) PR body standard sections beyond run report (e.g., checklist, coverage table)?
767 |
768 | 3) Default executor prompt fine-tuning once codex/gemini integration is available.
769 |
770 | 4) Where to store persistent TUI state (pane layout, last selection) in .taskmaster/state.json?
771 |
772 | ## Branch Naming
773 |
774 | - Include both the tag and the task id in the branch name to make lineage explicit.
775 |
776 | - Default pattern: <tag>/task-<id>[-slug] (e.g., master/task-12, tag-analytics/task-4-user-auth).
777 |
778 | - Configurable via .taskmaster/config.json: git.branchPattern supports tokens {tag}, {id}, {slug}.
779 |
780 | ## PR Base Branch
781 |
782 | - Use the repository’s default branch (detected via git) unless overridden.
783 |
784 | - Title format: Task #<id> [<tag>]: <title>.
785 |
786 | ## RPG Mapping (Repository Planning Graph)
787 |
788 | Functional nodes (capabilities):
789 |
790 | - Autopilot Orchestration → drives TDD loop and lifecycle
791 |
792 | - Test Generation (Surgical) → produces failing tests from subtask context
793 |
794 | - Test Execution + Coverage → runs suite, enforces thresholds
795 |
796 | - Git/Branch/PR Management → safe operations and PR creation
797 |
798 | - TUI/Terminal Integration → interactive control and visibility via tmux
799 |
800 | - MCP Integration → structured task/status/context operations
801 |
802 | Structural nodes (code organization):
803 |
804 | - packages/tm-core:
805 |
806 | - services/workflow-orchestrator.ts (new)
807 |
808 | - services/test-runner-adapter.ts (new)
809 |
810 | - services/git-adapter.ts (new)
811 |
812 | - existing: task-service.ts, task-execution-service.ts, executors/*
813 |
814 | - apps/cli:
815 |
816 | - src/commands/autopilot.command.ts (new)
817 |
818 | - src/ui/tui/ (new tmux/TUI helpers)
819 |
820 | - scripts/modules:
821 |
822 | - reuse utils/git-utils.js, task-manager/tag-management.js
823 |
824 | - .claude/agents/:
825 |
826 | - surgical-test-generator.md
827 |
828 | Edges (data/control flow):
829 |
830 | - Autopilot → Test Generation → Test Execution → Git Commit → loop
831 |
832 | - Autopilot → Git Adapter (branch, tag, PR)
833 |
834 | - Autopilot → TUI (event stream) → tmux pane control
835 |
836 | - Autopilot → MCP tools for task/status updates
837 |
838 | - Test Execution → Coverage gate → Autopilot decision
839 |
840 | Topological traversal (implementation order):
841 |
842 | 1) Git/Test adapters (foundations)
843 |
844 | 2) Orchestrator skeleton + events
845 |
846 | 3) CLI autopilot command and dry-run
847 |
848 | 4) Surgical test-gen integration and execution gate
849 |
850 | 5) PR creation, run reports, resumability
851 |
852 | ## Phased Roadmap
853 |
854 | - Phase 0: Spike
855 |
856 | - Implement CLI skeleton tm autopilot with dry-run showing planned steps from a real task + subtasks.
857 |
858 | - Detect test runner (package.json) and git state; render a preflight report.
859 |
860 | - Phase 1: Core Rails (State Machine & Orchestration)
861 |
862 | - Implement WorkflowOrchestrator in tm-core as a **state machine** that tracks TDD phases per subtask.
863 |
864 | - Orchestrator **guides** the current AI session (Claude Code/MCP client) rather than executing code itself.
865 |
866 | - Add Git/Test adapters for status checks and validation (not direct execution).
867 |
868 | - WorkflowOrchestrator API:
869 | - `getNextWorkUnit(taskId)` → returns next phase to execute (RED/GREEN/COMMIT) with context
870 | - `completeWorkUnit(taskId, subtaskId, phase, result)` → records completion and advances state
871 | - `getRunState(taskId)` → returns current progress and resumability data
872 |
873 | - MCP integration: expose work unit endpoints so Claude Code can query "what to do next" and report back.
874 |
875 | - Branch/tag mapping via existing tag-management APIs.
876 |
877 | - Run report persisted under .taskmaster/reports/runs/ with state checkpoints for resumability.
878 |
879 | - Phase 2: PR + Resumability
880 |
881 | - Add gh PR creation with well-formed body using the run report.
882 |
883 | - Introduce resumable checkpoints and --resume flag.
884 |
885 | - Add coverage enforcement and optional lint/format step.
886 |
887 | - Phase 3: Extensibility + Guardrails
888 |
889 | - Add support for basic pytest/go test adapters.
890 |
891 | - Add safeguards: diff preview mode, manual confirm gates, aggressive minimal-change prompts.
892 |
893 | - Optional: small TUI panel and extension panel leveraging the same run state file.
894 |
895 | ## References (Repo)
896 |
897 | - Test Workflow: .cursor/rules/test_workflow.mdc
898 |
899 | - Git Workflow: .cursor/rules/git_workflow.mdc
900 |
901 | - CLI: apps/cli/src/commands/start.command.ts, apps/cli/src/ui/components/*.ts
902 |
903 | - Core Services: packages/tm-core/src/services/task-service.ts, task-execution-service.ts
904 |
905 | - Executors: packages/tm-core/src/executors/*
906 |
907 | - Git Utilities: scripts/modules/utils/git-utils.js
908 |
909 | - Tag Management: scripts/modules/task-manager/tag-management.js
910 |
911 | - Surgical Test Generator: .claude/agents/surgical-test-generator.md
912 |
913 |
```
--------------------------------------------------------------------------------
/tests/unit/dependency-manager.test.js:
--------------------------------------------------------------------------------
```javascript
1 | /**
2 | * Dependency Manager module tests
3 | */
4 |
5 | import { jest } from '@jest/globals';
6 | import {
7 | sampleTasks,
8 | crossLevelDependencyTasks
9 | } from '../fixtures/sample-tasks.js';
10 |
11 | // Create mock functions that we can control in tests
12 | const mockTaskExists = jest.fn();
13 | const mockFormatTaskId = jest.fn();
14 | const mockFindCycles = jest.fn();
15 | const mockLog = jest.fn();
16 | const mockReadJSON = jest.fn();
17 | const mockWriteJSON = jest.fn();
18 |
19 | // Mock the utils module using the same pattern as move-task-cross-tag.test.js
20 | jest.mock('../../scripts/modules/utils.js', () => ({
21 | log: mockLog,
22 | readJSON: mockReadJSON,
23 | writeJSON: mockWriteJSON,
24 | taskExists: mockTaskExists,
25 | formatTaskId: mockFormatTaskId,
26 | findCycles: mockFindCycles,
27 | traverseDependencies: jest.fn(() => []),
28 | isSilentMode: jest.fn(() => true),
29 | findProjectRoot: jest.fn(() => '/test'),
30 | resolveEnvVariable: jest.fn(() => undefined),
31 | isEmpty: jest.fn((v) =>
32 | v == null
33 | ? true
34 | : Array.isArray(v)
35 | ? v.length === 0
36 | : typeof v === 'object'
37 | ? Object.keys(v).length === 0
38 | : false
39 | ),
40 | // Common extras
41 | enableSilentMode: jest.fn(),
42 | disableSilentMode: jest.fn(),
43 | getTaskManager: jest.fn(async () => ({})),
44 | getTagAwareFilePath: jest.fn((basePath, _tag, projectRoot = '.') => basePath),
45 | readComplexityReport: jest.fn(() => null)
46 | }));
47 |
48 | jest.mock('path');
49 | jest.mock('chalk', () => ({
50 | green: jest.fn((text) => `<green>${text}</green>`),
51 | yellow: jest.fn((text) => `<yellow>${text}</yellow>`),
52 | red: jest.fn((text) => `<red>${text}</red>`),
53 | cyan: jest.fn((text) => `<cyan>${text}</cyan>`),
54 | bold: jest.fn((text) => `<bold>${text}</bold>`)
55 | }));
56 |
57 | jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`));
58 |
59 | // Now import SUT after mocks are in place
60 | import {
61 | validateTaskDependencies,
62 | isCircularDependency,
63 | removeDuplicateDependencies,
64 | cleanupSubtaskDependencies,
65 | ensureAtLeastOneIndependentSubtask,
66 | validateAndFixDependencies,
67 | canMoveWithDependencies
68 | } from '../../scripts/modules/dependency-manager.js';
69 |
70 | jest.mock('../../scripts/modules/ui.js', () => ({
71 | displayBanner: jest.fn()
72 | }));
73 |
74 | jest.mock('../../scripts/modules/task-manager.js', () => ({
75 | generateTaskFiles: jest.fn()
76 | }));
77 |
78 | // Use a temporary path for test files - Jest will clean up the temp directory
79 | const TEST_TASKS_PATH = '/tmp/jest-test-tasks.json';
80 |
81 | describe('Dependency Manager Module', () => {
82 | beforeEach(() => {
83 | jest.clearAllMocks();
84 |
85 | // Set default implementations
86 | mockTaskExists.mockImplementation((tasks, id) => {
87 | if (Array.isArray(tasks)) {
88 | if (typeof id === 'string' && id.includes('.')) {
89 | const [taskId, subtaskId] = id.split('.').map(Number);
90 | const task = tasks.find((t) => t.id === taskId);
91 | return (
92 | task &&
93 | task.subtasks &&
94 | task.subtasks.some((st) => st.id === subtaskId)
95 | );
96 | }
97 | return tasks.some(
98 | (task) => task.id === (typeof id === 'string' ? parseInt(id, 10) : id)
99 | );
100 | }
101 | return false;
102 | });
103 |
104 | mockFormatTaskId.mockImplementation((id) => {
105 | if (typeof id === 'string' && id.includes('.')) {
106 | return id;
107 | }
108 | return parseInt(id, 10);
109 | });
110 |
111 | mockFindCycles.mockImplementation((tasks) => {
112 | // Simplified cycle detection for testing
113 | const dependencyMap = new Map();
114 |
115 | // Build dependency map
116 | tasks.forEach((task) => {
117 | if (task.dependencies) {
118 | dependencyMap.set(task.id, task.dependencies);
119 | }
120 | });
121 |
122 | const visited = new Set();
123 | const recursionStack = new Set();
124 |
125 | function dfs(taskId) {
126 | visited.add(taskId);
127 | recursionStack.add(taskId);
128 |
129 | const dependencies = dependencyMap.get(taskId) || [];
130 | for (const depId of dependencies) {
131 | if (!visited.has(depId)) {
132 | if (dfs(depId)) return true;
133 | } else if (recursionStack.has(depId)) {
134 | return true;
135 | }
136 | }
137 |
138 | recursionStack.delete(taskId);
139 | return false;
140 | }
141 |
142 | // Check for cycles starting from each unvisited node
143 | for (const taskId of dependencyMap.keys()) {
144 | if (!visited.has(taskId)) {
145 | if (dfs(taskId)) return true;
146 | }
147 | }
148 |
149 | return false;
150 | });
151 | });
152 |
153 | describe('isCircularDependency function', () => {
154 | test('should detect a direct circular dependency', () => {
155 | const tasks = [
156 | { id: 1, dependencies: [2] },
157 | { id: 2, dependencies: [1] }
158 | ];
159 |
160 | const result = isCircularDependency(tasks, 1);
161 | expect(result).toBe(true);
162 | });
163 |
164 | test('should detect an indirect circular dependency', () => {
165 | const tasks = [
166 | { id: 1, dependencies: [2] },
167 | { id: 2, dependencies: [3] },
168 | { id: 3, dependencies: [1] }
169 | ];
170 |
171 | const result = isCircularDependency(tasks, 1);
172 | expect(result).toBe(true);
173 | });
174 |
175 | test('should return false for non-circular dependencies', () => {
176 | const tasks = [
177 | { id: 1, dependencies: [2] },
178 | { id: 2, dependencies: [3] },
179 | { id: 3, dependencies: [] }
180 | ];
181 |
182 | const result = isCircularDependency(tasks, 1);
183 | expect(result).toBe(false);
184 | });
185 |
186 | test('should handle a task with no dependencies', () => {
187 | const tasks = [
188 | { id: 1, dependencies: [] },
189 | { id: 2, dependencies: [1] }
190 | ];
191 |
192 | const result = isCircularDependency(tasks, 1);
193 | expect(result).toBe(false);
194 | });
195 |
196 | test('should handle a task depending on itself', () => {
197 | const tasks = [{ id: 1, dependencies: [1] }];
198 |
199 | const result = isCircularDependency(tasks, 1);
200 | expect(result).toBe(true);
201 | });
202 |
203 | test('should handle subtask dependencies correctly', () => {
204 | const tasks = [
205 | {
206 | id: 1,
207 | dependencies: [],
208 | subtasks: [
209 | { id: 1, dependencies: ['1.2'] },
210 | { id: 2, dependencies: ['1.3'] },
211 | { id: 3, dependencies: ['1.1'] }
212 | ]
213 | }
214 | ];
215 |
216 | // This creates a circular dependency: 1.1 -> 1.2 -> 1.3 -> 1.1
217 | const result = isCircularDependency(tasks, '1.1', ['1.3', '1.2']);
218 | expect(result).toBe(true);
219 | });
220 |
221 | test('should allow non-circular subtask dependencies within same parent', () => {
222 | const tasks = [
223 | {
224 | id: 1,
225 | dependencies: [],
226 | subtasks: [
227 | { id: 1, dependencies: [] },
228 | { id: 2, dependencies: ['1.1'] },
229 | { id: 3, dependencies: ['1.2'] }
230 | ]
231 | }
232 | ];
233 |
234 | // This is a valid dependency chain: 1.3 -> 1.2 -> 1.1
235 | const result = isCircularDependency(tasks, '1.1', []);
236 | expect(result).toBe(false);
237 | });
238 |
239 | test('should properly handle dependencies between subtasks of the same parent', () => {
240 | const tasks = [
241 | {
242 | id: 1,
243 | dependencies: [],
244 | subtasks: [
245 | { id: 1, dependencies: [] },
246 | { id: 2, dependencies: ['1.1'] },
247 | { id: 3, dependencies: [] }
248 | ]
249 | }
250 | ];
251 |
252 | // Check if adding a dependency from subtask 1.3 to 1.2 creates a circular dependency
253 | // This should be false as 1.3 -> 1.2 -> 1.1 is a valid chain
254 | mockTaskExists.mockImplementation(() => true);
255 | const result = isCircularDependency(tasks, '1.3', ['1.2']);
256 | expect(result).toBe(false);
257 | });
258 |
259 | test('should correctly detect circular dependencies in subtasks of the same parent', () => {
260 | const tasks = [
261 | {
262 | id: 1,
263 | dependencies: [],
264 | subtasks: [
265 | { id: 1, dependencies: ['1.3'] },
266 | { id: 2, dependencies: ['1.1'] },
267 | { id: 3, dependencies: ['1.2'] }
268 | ]
269 | }
270 | ];
271 |
272 | // This creates a circular dependency: 1.1 -> 1.3 -> 1.2 -> 1.1
273 | mockTaskExists.mockImplementation(() => true);
274 | const result = isCircularDependency(tasks, '1.2', ['1.1']);
275 | expect(result).toBe(true);
276 | });
277 | });
278 |
279 | describe('validateTaskDependencies function', () => {
280 | test('should detect missing dependencies', () => {
281 | const tasks = [
282 | { id: 1, dependencies: [99] }, // 99 doesn't exist
283 | { id: 2, dependencies: [1] }
284 | ];
285 |
286 | const result = validateTaskDependencies(tasks);
287 |
288 | expect(result.valid).toBe(false);
289 | expect(result.issues.length).toBeGreaterThan(0);
290 | expect(result.issues[0].type).toBe('missing');
291 | expect(result.issues[0].taskId).toBe(1);
292 | expect(result.issues[0].dependencyId).toBe(99);
293 | });
294 |
295 | test('should detect circular dependencies', () => {
296 | const tasks = [
297 | { id: 1, dependencies: [2] },
298 | { id: 2, dependencies: [1] }
299 | ];
300 |
301 | const result = validateTaskDependencies(tasks);
302 |
303 | expect(result.valid).toBe(false);
304 | expect(result.issues.some((issue) => issue.type === 'circular')).toBe(
305 | true
306 | );
307 | });
308 |
309 | test('should detect self-dependencies', () => {
310 | const tasks = [{ id: 1, dependencies: [1] }];
311 |
312 | const result = validateTaskDependencies(tasks);
313 |
314 | expect(result.valid).toBe(false);
315 | expect(
316 | result.issues.some(
317 | (issue) => issue.type === 'self' && issue.taskId === 1
318 | )
319 | ).toBe(true);
320 | });
321 |
322 | test('should return valid for correct dependencies', () => {
323 | const tasks = [
324 | { id: 1, dependencies: [] },
325 | { id: 2, dependencies: [1] },
326 | { id: 3, dependencies: [1, 2] }
327 | ];
328 |
329 | const result = validateTaskDependencies(tasks);
330 |
331 | expect(result.valid).toBe(true);
332 | expect(result.issues.length).toBe(0);
333 | });
334 |
335 | test('should handle tasks with no dependencies property', () => {
336 | const tasks = [
337 | { id: 1 }, // Missing dependencies property
338 | { id: 2, dependencies: [1] }
339 | ];
340 |
341 | const result = validateTaskDependencies(tasks);
342 |
343 | // Should be valid since a missing dependencies property is interpreted as an empty array
344 | expect(result.valid).toBe(true);
345 | });
346 |
347 | test('should handle subtask dependencies correctly', () => {
348 | const tasks = [
349 | {
350 | id: 1,
351 | dependencies: [],
352 | subtasks: [
353 | { id: 1, dependencies: [] },
354 | { id: 2, dependencies: ['1.1'] }, // Valid - depends on another subtask
355 | { id: 3, dependencies: ['1.2'] } // Valid - depends on another subtask
356 | ]
357 | },
358 | {
359 | id: 2,
360 | dependencies: ['1.3'], // Valid - depends on a subtask from task 1
361 | subtasks: []
362 | }
363 | ];
364 |
365 | // Set up mock to handle subtask validation
366 | mockTaskExists.mockImplementation((tasks, id) => {
367 | if (typeof id === 'string' && id.includes('.')) {
368 | const [taskId, subtaskId] = id.split('.').map(Number);
369 | const task = tasks.find((t) => t.id === taskId);
370 | return (
371 | task &&
372 | task.subtasks &&
373 | task.subtasks.some((st) => st.id === subtaskId)
374 | );
375 | }
376 | return tasks.some((task) => task.id === parseInt(id, 10));
377 | });
378 |
379 | const result = validateTaskDependencies(tasks);
380 |
381 | expect(result.valid).toBe(true);
382 | expect(result.issues.length).toBe(0);
383 | });
384 |
385 | test('should detect missing subtask dependencies', () => {
386 | const tasks = [
387 | {
388 | id: 1,
389 | dependencies: [],
390 | subtasks: [
391 | { id: 1, dependencies: ['1.4'] }, // Invalid - subtask 4 doesn't exist
392 | { id: 2, dependencies: ['2.1'] } // Invalid - task 2 has no subtasks
393 | ]
394 | },
395 | {
396 | id: 2,
397 | dependencies: [],
398 | subtasks: []
399 | }
400 | ];
401 |
402 | // Mock taskExists to correctly identify missing subtasks
403 | mockTaskExists.mockImplementation((taskArray, depId) => {
404 | if (typeof depId === 'string' && depId === '1.4') {
405 | return false; // Subtask 1.4 doesn't exist
406 | }
407 | if (typeof depId === 'string' && depId === '2.1') {
408 | return false; // Subtask 2.1 doesn't exist
409 | }
410 | return true; // All other dependencies exist
411 | });
412 |
413 | const result = validateTaskDependencies(tasks);
414 |
415 | expect(result.valid).toBe(false);
416 | expect(result.issues.length).toBeGreaterThan(0);
417 | // Should detect missing subtask dependencies
418 | expect(
419 | result.issues.some(
420 | (issue) =>
421 | issue.type === 'missing' &&
422 | String(issue.taskId) === '1.1' &&
423 | String(issue.dependencyId) === '1.4'
424 | )
425 | ).toBe(true);
426 | });
427 |
428 | test('should detect circular dependencies between subtasks', () => {
429 | const tasks = [
430 | {
431 | id: 1,
432 | dependencies: [],
433 | subtasks: [
434 | { id: 1, dependencies: ['1.2'] },
435 | { id: 2, dependencies: ['1.1'] } // Creates a circular dependency with 1.1
436 | ]
437 | }
438 | ];
439 |
440 | // Mock isCircularDependency for subtasks
441 | mockFindCycles.mockReturnValue(true);
442 |
443 | const result = validateTaskDependencies(tasks);
444 |
445 | expect(result.valid).toBe(false);
446 | expect(result.issues.some((issue) => issue.type === 'circular')).toBe(
447 | true
448 | );
449 | });
450 |
451 | test('should properly validate dependencies between subtasks of the same parent', () => {
452 | const tasks = [
453 | {
454 | id: 23,
455 | dependencies: [],
456 | subtasks: [
457 | { id: 8, dependencies: ['23.13'] },
458 | { id: 10, dependencies: ['23.8'] },
459 | { id: 13, dependencies: [] }
460 | ]
461 | }
462 | ];
463 |
464 | // Mock taskExists to validate the subtask dependencies
465 | mockTaskExists.mockImplementation((taskArray, id) => {
466 | if (typeof id === 'string') {
467 | if (id === '23.8' || id === '23.10' || id === '23.13') {
468 | return true;
469 | }
470 | }
471 | return false;
472 | });
473 |
474 | const result = validateTaskDependencies(tasks);
475 |
476 | expect(result.valid).toBe(true);
477 | expect(result.issues.length).toBe(0);
478 | });
479 | });
480 |
481 | describe('removeDuplicateDependencies function', () => {
482 | test('should remove duplicate dependencies from tasks', () => {
483 | const tasksData = {
484 | tasks: [
485 | { id: 1, dependencies: [2, 2, 3, 3, 3] },
486 | { id: 2, dependencies: [3] },
487 | { id: 3, dependencies: [] }
488 | ]
489 | };
490 |
491 | const result = removeDuplicateDependencies(tasksData);
492 |
493 | expect(result.tasks[0].dependencies).toEqual([2, 3]);
494 | expect(result.tasks[1].dependencies).toEqual([3]);
495 | expect(result.tasks[2].dependencies).toEqual([]);
496 | });
497 |
498 | test('should handle empty dependencies array', () => {
499 | const tasksData = {
500 | tasks: [
501 | { id: 1, dependencies: [] },
502 | { id: 2, dependencies: [1] }
503 | ]
504 | };
505 |
506 | const result = removeDuplicateDependencies(tasksData);
507 |
508 | expect(result.tasks[0].dependencies).toEqual([]);
509 | expect(result.tasks[1].dependencies).toEqual([1]);
510 | });
511 |
512 | test('should handle tasks with no dependencies property', () => {
513 | const tasksData = {
514 | tasks: [
515 | { id: 1 }, // No dependencies property
516 | { id: 2, dependencies: [1] }
517 | ]
518 | };
519 |
520 | const result = removeDuplicateDependencies(tasksData);
521 |
522 | expect(result.tasks[0]).not.toHaveProperty('dependencies');
523 | expect(result.tasks[1].dependencies).toEqual([1]);
524 | });
525 | });
526 |
527 | describe('cleanupSubtaskDependencies function', () => {
528 | test('should remove dependencies to non-existent subtasks', () => {
529 | const tasksData = {
530 | tasks: [
531 | {
532 | id: 1,
533 | dependencies: [],
534 | subtasks: [
535 | { id: 1, dependencies: [] },
536 | { id: 2, dependencies: [3] } // Dependency 3 doesn't exist
537 | ]
538 | },
539 | {
540 | id: 2,
541 | dependencies: ['1.2'], // Valid subtask dependency
542 | subtasks: [
543 | { id: 1, dependencies: ['1.1'] } // Valid subtask dependency
544 | ]
545 | }
546 | ]
547 | };
548 |
549 | const result = cleanupSubtaskDependencies(tasksData);
550 |
551 | // Should remove the invalid dependency to subtask 3
552 | expect(result.tasks[0].subtasks[1].dependencies).toEqual([]);
553 | // Should keep valid dependencies
554 | expect(result.tasks[1].dependencies).toEqual(['1.2']);
555 | expect(result.tasks[1].subtasks[0].dependencies).toEqual(['1.1']);
556 | });
557 |
558 | test('should handle tasks without subtasks', () => {
559 | const tasksData = {
560 | tasks: [
561 | { id: 1, dependencies: [] },
562 | { id: 2, dependencies: [1] }
563 | ]
564 | };
565 |
566 | const result = cleanupSubtaskDependencies(tasksData);
567 |
568 | // Should return the original data unchanged
569 | expect(result).toEqual(tasksData);
570 | });
571 | });
572 |
573 | describe('ensureAtLeastOneIndependentSubtask function', () => {
574 | test('should clear dependencies of first subtask if none are independent', () => {
575 | const tasksData = {
576 | tasks: [
577 | {
578 | id: 1,
579 | subtasks: [
580 | { id: 1, dependencies: [2] },
581 | { id: 2, dependencies: [1] }
582 | ]
583 | }
584 | ]
585 | };
586 |
587 | const result = ensureAtLeastOneIndependentSubtask(tasksData);
588 |
589 | expect(result).toBe(true);
590 | expect(tasksData.tasks[0].subtasks[0].dependencies).toEqual([]);
591 | expect(tasksData.tasks[0].subtasks[1].dependencies).toEqual([1]);
592 | });
593 |
594 | test('should not modify tasks if at least one subtask is independent', () => {
595 | const tasksData = {
596 | tasks: [
597 | {
598 | id: 1,
599 | subtasks: [
600 | { id: 1, dependencies: [] },
601 | { id: 2, dependencies: [1] }
602 | ]
603 | }
604 | ]
605 | };
606 |
607 | const result = ensureAtLeastOneIndependentSubtask(tasksData);
608 |
609 | expect(result).toBe(false);
610 | expect(tasksData.tasks[0].subtasks[0].dependencies).toEqual([]);
611 | expect(tasksData.tasks[0].subtasks[1].dependencies).toEqual([1]);
612 | });
613 |
614 | test('should handle tasks without subtasks', () => {
615 | const tasksData = {
616 | tasks: [{ id: 1 }, { id: 2, dependencies: [1] }]
617 | };
618 |
619 | const result = ensureAtLeastOneIndependentSubtask(tasksData);
620 |
621 | expect(result).toBe(false);
622 | expect(tasksData).toEqual({
623 | tasks: [{ id: 1 }, { id: 2, dependencies: [1] }]
624 | });
625 | });
626 |
627 | test('should handle empty subtasks array', () => {
628 | const tasksData = {
629 | tasks: [{ id: 1, subtasks: [] }]
630 | };
631 |
632 | const result = ensureAtLeastOneIndependentSubtask(tasksData);
633 |
634 | expect(result).toBe(false);
635 | expect(tasksData).toEqual({
636 | tasks: [{ id: 1, subtasks: [] }]
637 | });
638 | });
639 | });
640 |
641 | describe('validateAndFixDependencies function', () => {
642 | test('should fix multiple dependency issues and return true if changes made', () => {
643 | const tasksData = {
644 | tasks: [
645 | {
646 | id: 1,
647 | dependencies: [1, 1, 99], // Self-dependency and duplicate and invalid dependency
648 | subtasks: [
649 | { id: 1, dependencies: [2, 2] }, // Duplicate dependencies
650 | { id: 2, dependencies: [1] }
651 | ]
652 | },
653 | {
654 | id: 2,
655 | dependencies: [1],
656 | subtasks: [
657 | { id: 1, dependencies: [99] } // Invalid dependency
658 | ]
659 | }
660 | ]
661 | };
662 |
663 | // Mock taskExists for validating dependencies
664 | mockTaskExists.mockImplementation((tasks, id) => {
665 | // Convert id to string for comparison
666 | const idStr = String(id);
667 |
668 | // Handle subtask references (e.g., "1.2")
669 | if (idStr.includes('.')) {
670 | const [parentId, subtaskId] = idStr.split('.').map(Number);
671 | const task = tasks.find((t) => t.id === parentId);
672 | return (
673 | task &&
674 | task.subtasks &&
675 | task.subtasks.some((st) => st.id === subtaskId)
676 | );
677 | }
678 |
679 | // Handle regular task references
680 | const taskId = parseInt(idStr, 10);
681 | return taskId === 1 || taskId === 2; // Only tasks 1 and 2 exist
682 | });
683 |
684 | // Make a copy for verification that original is modified
685 | const originalData = JSON.parse(JSON.stringify(tasksData));
686 |
687 | const result = validateAndFixDependencies(tasksData);
688 |
689 | expect(result).toBe(true);
690 | // Check that data has been modified
691 | expect(tasksData).not.toEqual(originalData);
692 |
693 | // Check specific changes
694 | // 1. Self-dependency removed
695 | expect(tasksData.tasks[0].dependencies).not.toContain(1);
696 | // 2. Invalid dependency removed
697 | expect(tasksData.tasks[0].dependencies).not.toContain(99);
698 | // 3. Dependencies have been deduplicated
699 | if (tasksData.tasks[0].subtasks[0].dependencies.length > 0) {
700 | expect(tasksData.tasks[0].subtasks[0].dependencies).toEqual(
701 | expect.arrayContaining([])
702 | );
703 | }
704 | // 4. Invalid subtask dependency removed
705 | expect(tasksData.tasks[1].subtasks[0].dependencies).toEqual([]);
706 |
707 | // IMPORTANT: Verify no calls to writeJSON with actual tasks.json
708 | expect(mockWriteJSON).not.toHaveBeenCalledWith(
709 | 'tasks/tasks.json',
710 | expect.anything(),
711 | expect.anything(),
712 | expect.anything()
713 | );
714 | });
715 |
716 | test('should return false if no changes needed', () => {
717 | const tasksData = {
718 | tasks: [
719 | {
720 | id: 1,
721 | dependencies: [],
722 | subtasks: [
723 | { id: 1, dependencies: [] }, // Already has an independent subtask
724 | { id: 2, dependencies: ['1.1'] }
725 | ]
726 | },
727 | {
728 | id: 2,
729 | dependencies: [1]
730 | }
731 | ]
732 | };
733 |
734 | // Mock taskExists to validate all dependencies as valid
735 | mockTaskExists.mockImplementation((tasks, id) => {
736 | // Convert id to string for comparison
737 | const idStr = String(id);
738 |
739 | // Handle subtask references
740 | if (idStr.includes('.')) {
741 | const [parentId, subtaskId] = idStr.split('.').map(Number);
742 | const task = tasks.find((t) => t.id === parentId);
743 | return (
744 | task &&
745 | task.subtasks &&
746 | task.subtasks.some((st) => st.id === subtaskId)
747 | );
748 | }
749 |
750 | // Handle regular task references
751 | const taskId = parseInt(idStr, 10);
752 | return taskId === 1 || taskId === 2;
753 | });
754 |
755 | const originalData = JSON.parse(JSON.stringify(tasksData));
756 | const result = validateAndFixDependencies(tasksData);
757 |
758 | expect(result).toBe(false);
759 | // Verify data is unchanged
760 | expect(tasksData).toEqual(originalData);
761 |
762 | // IMPORTANT: Verify no calls to writeJSON with actual tasks.json
763 | expect(mockWriteJSON).not.toHaveBeenCalledWith(
764 | 'tasks/tasks.json',
765 | expect.anything(),
766 | expect.anything(),
767 | expect.anything()
768 | );
769 | });
770 |
771 | test('should handle invalid input', () => {
772 | expect(validateAndFixDependencies(null)).toBe(false);
773 | expect(validateAndFixDependencies({})).toBe(false);
774 | expect(validateAndFixDependencies({ tasks: null })).toBe(false);
775 | expect(validateAndFixDependencies({ tasks: 'not an array' })).toBe(false);
776 |
777 | // IMPORTANT: Verify no calls to writeJSON with actual tasks.json
778 | expect(mockWriteJSON).not.toHaveBeenCalledWith(
779 | 'tasks/tasks.json',
780 | expect.anything(),
781 | expect.anything(),
782 | expect.anything()
783 | );
784 | });
785 |
786 | test('should save changes when tasksPath is provided', () => {
787 | const tasksData = {
788 | tasks: [
789 | {
790 | id: 1,
791 | dependencies: [1, 1], // Self-dependency and duplicate
792 | subtasks: [
793 | { id: 1, dependencies: [99] } // Invalid dependency
794 | ]
795 | }
796 | ]
797 | };
798 |
799 | // Mock taskExists for this specific test
800 | mockTaskExists.mockImplementation((tasks, id) => {
801 | // Convert id to string for comparison
802 | const idStr = String(id);
803 |
804 | // Handle subtask references
805 | if (idStr.includes('.')) {
806 | const [parentId, subtaskId] = idStr.split('.').map(Number);
807 | const task = tasks.find((t) => t.id === parentId);
808 | return (
809 | task &&
810 | task.subtasks &&
811 | task.subtasks.some((st) => st.id === subtaskId)
812 | );
813 | }
814 |
815 | // Handle regular task references
816 | const taskId = parseInt(idStr, 10);
817 | return taskId === 1; // Only task 1 exists
818 | });
819 |
820 | // Copy the original data to verify changes
821 | const originalData = JSON.parse(JSON.stringify(tasksData));
822 |
823 | // Call the function with our test path instead of the actual tasks.json
824 | const result = validateAndFixDependencies(tasksData, TEST_TASKS_PATH);
825 |
826 | // First verify that the result is true (changes were made)
827 | expect(result).toBe(true);
828 |
829 | // Verify the data was modified
830 | expect(tasksData).not.toEqual(originalData);
831 |
832 | // IMPORTANT: Verify no calls to writeJSON with actual tasks.json
833 | expect(mockWriteJSON).not.toHaveBeenCalledWith(
834 | 'tasks/tasks.json',
835 | expect.anything(),
836 | expect.anything(),
837 | expect.anything()
838 | );
839 | });
840 | });
841 |
842 | describe('canMoveWithDependencies', () => {
843 | it('should return canMove: false when conflicts exist', () => {
844 | const allTasks = [
845 | {
846 | id: 1,
847 | tag: 'source',
848 | dependencies: [2],
849 | title: 'Task 1'
850 | },
851 | {
852 | id: 2,
853 | tag: 'other',
854 | dependencies: [],
855 | title: 'Task 2'
856 | }
857 | ];
858 |
859 | const result = canMoveWithDependencies('1', 'source', 'target', allTasks);
860 |
861 | expect(result.canMove).toBe(false);
862 | expect(result.conflicts).toBeDefined();
863 | expect(result.conflicts.length).toBeGreaterThan(0);
864 | expect(result.dependentTaskIds).toBeDefined();
865 | });
866 |
867 | it('should return canMove: true when no conflicts exist', () => {
868 | const allTasks = [
869 | {
870 | id: 1,
871 | tag: 'source',
872 | dependencies: [],
873 | title: 'Task 1'
874 | },
875 | {
876 | id: 2,
877 | tag: 'target',
878 | dependencies: [],
879 | title: 'Task 2'
880 | }
881 | ];
882 |
883 | const result = canMoveWithDependencies('1', 'source', 'target', allTasks);
884 |
885 | expect(result.canMove).toBe(true);
886 | expect(result.conflicts).toBeDefined();
887 | expect(result.conflicts.length).toBe(0);
888 | expect(result.dependentTaskIds).toBeDefined();
889 | expect(result.dependentTaskIds.length).toBe(0);
890 | });
891 |
892 | it('should handle subtask lookup correctly', () => {
893 | const allTasks = [
894 | {
895 | id: 1,
896 | tag: 'source',
897 | dependencies: [],
898 | title: 'Parent Task',
899 | subtasks: [
900 | {
901 | id: 1,
902 | dependencies: [2],
903 | title: 'Subtask 1'
904 | }
905 | ]
906 | },
907 | {
908 | id: 2,
909 | tag: 'other',
910 | dependencies: [],
911 | title: 'Task 2'
912 | }
913 | ];
914 |
915 | const result = canMoveWithDependencies(
916 | '1.1',
917 | 'source',
918 | 'target',
919 | allTasks
920 | );
921 |
922 | expect(result.canMove).toBe(false);
923 | expect(result.conflicts).toBeDefined();
924 | expect(result.conflicts.length).toBeGreaterThan(0);
925 | });
926 |
927 | it('should return error when task not found', () => {
928 | const allTasks = [
929 | {
930 | id: 1,
931 | tag: 'source',
932 | dependencies: [],
933 | title: 'Task 1'
934 | }
935 | ];
936 |
937 | const result = canMoveWithDependencies(
938 | '999',
939 | 'source',
940 | 'target',
941 | allTasks
942 | );
943 |
944 | expect(result.canMove).toBe(false);
945 | expect(result.error).toBe('Task not found');
946 | expect(result.dependentTaskIds).toEqual([]);
947 | expect(result.conflicts).toEqual([]);
948 | });
949 | });
950 |
951 | describe('Cross-level dependency tests (Issue #542)', () => {
952 | let originalExit;
953 |
954 | beforeEach(async () => {
955 | // Ensure a fresh module instance so ESM mocks apply to dynamic imports
956 | jest.resetModules();
957 | originalExit = process.exit;
958 | process.exit = jest.fn();
959 |
960 | // For ESM dynamic imports, use the same pattern
961 | await jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
962 | log: mockLog,
963 | readJSON: mockReadJSON,
964 | writeJSON: mockWriteJSON,
965 | taskExists: mockTaskExists,
966 | formatTaskId: mockFormatTaskId,
967 | findCycles: mockFindCycles,
968 | traverseDependencies: jest.fn(() => []),
969 | isSilentMode: jest.fn(() => true),
970 | findProjectRoot: jest.fn(() => '/test'),
971 | resolveEnvVariable: jest.fn(() => undefined),
972 | isEmpty: jest.fn((v) =>
973 | v == null
974 | ? true
975 | : Array.isArray(v)
976 | ? v.length === 0
977 | : typeof v === 'object'
978 | ? Object.keys(v).length === 0
979 | : false
980 | ),
981 | enableSilentMode: jest.fn(),
982 | disableSilentMode: jest.fn(),
983 | getTaskManager: jest.fn(async () => ({})),
984 | getTagAwareFilePath: jest.fn(
985 | (basePath, _tag, projectRoot = '.') => basePath
986 | ),
987 | readComplexityReport: jest.fn(() => null)
988 | }));
989 |
990 | // Also mock transitive imports to keep dependency surface minimal
991 | await jest.unstable_mockModule('../../scripts/modules/ui.js', () => ({
992 | displayBanner: jest.fn()
993 | }));
994 | await jest.unstable_mockModule(
995 | '../../scripts/modules/task-manager/generate-task-files.js',
996 | () => ({ default: jest.fn() })
997 | );
998 | // Set up test data that matches the issue report
999 | // Clone fixture data before each test to prevent mutation issues
1000 | mockReadJSON.mockImplementation(() =>
1001 | structuredClone(crossLevelDependencyTasks)
1002 | );
1003 |
1004 | // Configure mockTaskExists to properly validate cross-level dependencies
1005 | mockTaskExists.mockImplementation((tasks, taskId) => {
1006 | if (typeof taskId === 'string' && taskId.includes('.')) {
1007 | const [parentId, subtaskId] = taskId.split('.').map(Number);
1008 | const task = tasks.find((t) => t.id === parentId);
1009 | return (
1010 | task &&
1011 | task.subtasks &&
1012 | task.subtasks.some((st) => st.id === subtaskId)
1013 | );
1014 | }
1015 |
1016 | const numericId =
1017 | typeof taskId === 'string' ? parseInt(taskId, 10) : taskId;
1018 | return tasks.some((task) => task.id === numericId);
1019 | });
1020 |
1021 | mockFormatTaskId.mockImplementation((id) => {
1022 | if (typeof id === 'string' && id.includes('.')) return id; // keep dot notation
1023 | return parseInt(id, 10); // normalize top-level task IDs to number
1024 | });
1025 | });
1026 |
1027 | afterEach(() => {
1028 | process.exit = originalExit;
1029 | });
1030 |
1031 | test('should allow subtask to depend on top-level task', async () => {
1032 | const { addDependency } = await import(
1033 | '../../scripts/modules/dependency-manager.js'
1034 | );
1035 |
1036 | // Test the specific scenario from Issue #542: subtask 2.2 depending on task 11
1037 | await addDependency(TEST_TASKS_PATH, '2.2', 11, { projectRoot: '/test' });
1038 |
1039 | // Verify we wrote to the test path (and not the real tasks.json)
1040 | expect(mockWriteJSON).toHaveBeenCalledWith(
1041 | TEST_TASKS_PATH,
1042 | expect.anything(),
1043 | '/test',
1044 | undefined
1045 | );
1046 | expect(mockWriteJSON).not.toHaveBeenCalledWith(
1047 | 'tasks/tasks.json',
1048 | expect.anything(),
1049 | expect.anything(),
1050 | expect.anything()
1051 | );
1052 | // Get the specific write call for TEST_TASKS_PATH
1053 | const writeCall = mockWriteJSON.mock.calls.find(
1054 | ([p]) => p === TEST_TASKS_PATH
1055 | );
1056 | expect(writeCall).toBeDefined();
1057 | const savedData = writeCall[1];
1058 | const parent2 = savedData.tasks.find((t) => t.id === 2);
1059 | const subtask22 = parent2.subtasks.find((st) => st.id === 2);
1060 |
1061 | // Verify the dependency was actually added to subtask 2.2
1062 | expect(subtask22.dependencies).toContain(11);
1063 | // Also verify a success log was emitted
1064 | const successCall = mockLog.mock.calls.find(
1065 | ([level]) => level === 'success'
1066 | );
1067 | expect(successCall).toBeDefined();
1068 | expect(successCall[1]).toContain('2.2');
1069 | expect(successCall[1]).toContain('11');
1070 | });
1071 |
1072 | test('should allow top-level task to depend on subtask', async () => {
1073 | const { addDependency } = await import(
1074 | '../../scripts/modules/dependency-manager.js'
1075 | );
1076 |
1077 | // Test reverse scenario: task 11 depending on subtask 2.1
1078 | await addDependency(TEST_TASKS_PATH, 11, '2.1', { projectRoot: '/test' });
1079 |
1080 | // Stronger assertions for writeJSON call and locating the correct task
1081 | expect(mockWriteJSON).toHaveBeenCalledWith(
1082 | TEST_TASKS_PATH,
1083 | expect.anything(),
1084 | '/test',
1085 | undefined
1086 | );
1087 | expect(mockWriteJSON).not.toHaveBeenCalledWith(
1088 | 'tasks/tasks.json',
1089 | expect.anything(),
1090 | expect.anything(),
1091 | expect.anything()
1092 | );
1093 | const writeCall = mockWriteJSON.mock.calls.find(
1094 | ([p]) => p === TEST_TASKS_PATH
1095 | );
1096 | expect(writeCall).toBeDefined();
1097 | const savedData = writeCall[1];
1098 | const task11 = savedData.tasks.find((t) => t.id === 11);
1099 |
1100 | // Verify the dependency was actually added to task 11
1101 | expect(task11.dependencies).toContain('2.1');
1102 | // Verify a success log was emitted mentioning both task 11 and subtask 2.1
1103 | const successCall = mockLog.mock.calls.find(
1104 | ([level]) => level === 'success'
1105 | );
1106 | expect(successCall).toBeDefined();
1107 | expect(successCall[1]).toContain('11');
1108 | expect(successCall[1]).toContain('2.1');
1109 | });
1110 |
1111 | test('should properly validate cross-level dependencies exist', async () => {
1112 | // Test that validation correctly identifies when a cross-level dependency target doesn't exist
1113 | mockTaskExists.mockImplementation((tasks, taskId) => {
1114 | // Simulate task 99 not existing
1115 | if (taskId === '99' || taskId === 99) {
1116 | return false;
1117 | }
1118 |
1119 | if (typeof taskId === 'string' && taskId.includes('.')) {
1120 | const [parentId, subtaskId] = taskId.split('.').map(Number);
1121 | const task = tasks.find((t) => t.id === parentId);
1122 | return (
1123 | task &&
1124 | task.subtasks &&
1125 | task.subtasks.some((st) => st.id === subtaskId)
1126 | );
1127 | }
1128 |
1129 | const numericId =
1130 | typeof taskId === 'string' ? parseInt(taskId, 10) : taskId;
1131 | return tasks.some((task) => task.id === numericId);
1132 | });
1133 |
1134 | const { addDependency } = await import(
1135 | '../../scripts/modules/dependency-manager.js'
1136 | );
1137 |
1138 | const exitError = new Error('process.exit invoked');
1139 | process.exit.mockImplementation(() => {
1140 | throw exitError;
1141 | });
1142 |
1143 | await expect(
1144 | addDependency(TEST_TASKS_PATH, '2.2', 99, { projectRoot: '/test' })
1145 | ).rejects.toBe(exitError);
1146 |
1147 | expect(process.exit).toHaveBeenCalledWith(1);
1148 | expect(mockWriteJSON).not.toHaveBeenCalled();
1149 | // Verify that an error was reported to the user
1150 | expect(mockLog).toHaveBeenCalled();
1151 | });
1152 |
1153 | test('should remove top-level task dependency from a subtask', async () => {
1154 | const { addDependency, removeDependency } = await import(
1155 | '../../scripts/modules/dependency-manager.js'
1156 | );
1157 |
1158 | // Start with cloned data and add 11 to 2.2
1159 | await addDependency(TEST_TASKS_PATH, '2.2', 11, { projectRoot: '/test' });
1160 |
1161 | // Get the saved data from the add operation
1162 | const addWriteCall = mockWriteJSON.mock.calls.find(
1163 | ([p]) => p === TEST_TASKS_PATH
1164 | );
1165 | expect(addWriteCall).toBeDefined();
1166 | const dataWithDep = addWriteCall[1];
1167 |
1168 | // Verify the dependency was added
1169 | const subtask22AfterAdd = dataWithDep.tasks
1170 | .find((t) => t.id === 2)
1171 | .subtasks.find((st) => st.id === 2);
1172 | expect(subtask22AfterAdd.dependencies).toContain(11);
1173 |
1174 | // Clear mocks and re-setup mockReadJSON with the modified data
1175 | jest.clearAllMocks();
1176 | mockReadJSON.mockImplementation(() => structuredClone(dataWithDep));
1177 |
1178 | await removeDependency(TEST_TASKS_PATH, '2.2', 11, {
1179 | projectRoot: '/test'
1180 | });
1181 |
1182 | const writeCall = mockWriteJSON.mock.calls.find(
1183 | ([p]) => p === TEST_TASKS_PATH
1184 | );
1185 | expect(writeCall).toBeDefined();
1186 | const saved = writeCall[1];
1187 | const subtask22 = saved.tasks
1188 | .find((t) => t.id === 2)
1189 | .subtasks.find((st) => st.id === 2);
1190 | expect(subtask22.dependencies).not.toContain(11);
1191 | // Verify success log was emitted
1192 | const successCall = mockLog.mock.calls.find(
1193 | ([level]) => level === 'success'
1194 | );
1195 | expect(successCall).toBeDefined();
1196 | expect(successCall[1]).toContain('2.2');
1197 | expect(successCall[1]).toContain('11');
1198 | });
1199 |
1200 | test('should remove subtask dependency from a top-level task', async () => {
1201 | const { addDependency, removeDependency } = await import(
1202 | '../../scripts/modules/dependency-manager.js'
1203 | );
1204 |
1205 | // Add subtask dependency to task 11
1206 | await addDependency(TEST_TASKS_PATH, 11, '2.1', { projectRoot: '/test' });
1207 |
1208 | // Get the saved data from the add operation
1209 | const addWriteCall = mockWriteJSON.mock.calls.find(
1210 | ([p]) => p === TEST_TASKS_PATH
1211 | );
1212 | expect(addWriteCall).toBeDefined();
1213 | const dataWithDep = addWriteCall[1];
1214 |
1215 | // Verify the dependency was added
1216 | const task11AfterAdd = dataWithDep.tasks.find((t) => t.id === 11);
1217 | expect(task11AfterAdd.dependencies).toContain('2.1');
1218 |
1219 | // Clear mocks and re-setup mockReadJSON with the modified data
1220 | jest.clearAllMocks();
1221 | mockReadJSON.mockImplementation(() => structuredClone(dataWithDep));
1222 |
1223 | await removeDependency(TEST_TASKS_PATH, 11, '2.1', {
1224 | projectRoot: '/test'
1225 | });
1226 |
1227 | const writeCall = mockWriteJSON.mock.calls.find(
1228 | ([p]) => p === TEST_TASKS_PATH
1229 | );
1230 | expect(writeCall).toBeDefined();
1231 | const saved = writeCall[1];
1232 | const task11 = saved.tasks.find((t) => t.id === 11);
1233 | expect(task11.dependencies).not.toContain('2.1');
1234 | // Verify success log was emitted
1235 | const successCall = mockLog.mock.calls.find(
1236 | ([level]) => level === 'success'
1237 | );
1238 | expect(successCall).toBeDefined();
1239 | expect(successCall[1]).toContain('11');
1240 | expect(successCall[1]).toContain('2.1');
1241 | });
1242 | });
1243 | });
1244 |
```