#
tokens: 46811/50000 4/821 files (page 37/52)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 37 of 52. 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
│   ├── agents
│   │   ├── task-checker.md
│   │   ├── task-executor.md
│   │   └── task-orchestrator.md
│   ├── commands
│   │   ├── dedupe.md
│   │   └── tm
│   │       ├── add-dependency
│   │       │   └── add-dependency.md
│   │       ├── add-subtask
│   │       │   ├── add-subtask.md
│   │       │   └── convert-task-to-subtask.md
│   │       ├── add-task
│   │       │   └── add-task.md
│   │       ├── analyze-complexity
│   │       │   └── analyze-complexity.md
│   │       ├── complexity-report
│   │       │   └── complexity-report.md
│   │       ├── expand
│   │       │   ├── expand-all-tasks.md
│   │       │   └── expand-task.md
│   │       ├── fix-dependencies
│   │       │   └── fix-dependencies.md
│   │       ├── generate
│   │       │   └── generate-tasks.md
│   │       ├── help.md
│   │       ├── init
│   │       │   ├── init-project-quick.md
│   │       │   └── init-project.md
│   │       ├── learn.md
│   │       ├── list
│   │       │   ├── list-tasks-by-status.md
│   │       │   ├── list-tasks-with-subtasks.md
│   │       │   └── list-tasks.md
│   │       ├── models
│   │       │   ├── setup-models.md
│   │       │   └── view-models.md
│   │       ├── next
│   │       │   └── next-task.md
│   │       ├── parse-prd
│   │       │   ├── parse-prd-with-research.md
│   │       │   └── parse-prd.md
│   │       ├── remove-dependency
│   │       │   └── remove-dependency.md
│   │       ├── remove-subtask
│   │       │   └── remove-subtask.md
│   │       ├── remove-subtasks
│   │       │   ├── remove-all-subtasks.md
│   │       │   └── remove-subtasks.md
│   │       ├── remove-task
│   │       │   └── remove-task.md
│   │       ├── set-status
│   │       │   ├── to-cancelled.md
│   │       │   ├── to-deferred.md
│   │       │   ├── to-done.md
│   │       │   ├── to-in-progress.md
│   │       │   ├── to-pending.md
│   │       │   └── to-review.md
│   │       ├── setup
│   │       │   ├── install-taskmaster.md
│   │       │   └── quick-install-taskmaster.md
│   │       ├── show
│   │       │   └── show-task.md
│   │       ├── status
│   │       │   └── project-status.md
│   │       ├── sync-readme
│   │       │   └── sync-readme.md
│   │       ├── tm-main.md
│   │       ├── update
│   │       │   ├── update-single-task.md
│   │       │   ├── update-task.md
│   │       │   └── update-tasks-from-id.md
│   │       ├── utils
│   │       │   └── analyze-project.md
│   │       ├── validate-dependencies
│   │       │   └── validate-dependencies.md
│   │       └── workflows
│   │           ├── auto-implement-tasks.md
│   │           ├── command-pipeline.md
│   │           └── smart-workflow.md
│   └── TM_COMMANDS_GUIDE.md
├── .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
│   └── 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
│   │   ├── 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
│   │   ├── test-prd.txt
│   │   └── tm-core-phase-1.txt
│   ├── reports
│   │   ├── task-complexity-report_cc-kiro-hooks.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.txt
├── .vscode
│   ├── extensions.json
│   └── settings.json
├── apps
│   ├── cli
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   ├── commands
│   │   │   │   ├── auth.command.ts
│   │   │   │   ├── context.command.ts
│   │   │   │   ├── list.command.ts
│   │   │   │   ├── set-status.command.ts
│   │   │   │   ├── show.command.ts
│   │   │   │   └── start.command.ts
│   │   │   ├── index.ts
│   │   │   ├── ui
│   │   │   │   ├── components
│   │   │   │   │   ├── dashboard.component.ts
│   │   │   │   │   ├── header.component.ts
│   │   │   │   │   ├── index.ts
│   │   │   │   │   ├── next-task.component.ts
│   │   │   │   │   ├── suggested-steps.component.ts
│   │   │   │   │   └── task-detail.component.ts
│   │   │   │   └── index.ts
│   │   │   └── utils
│   │   │       ├── auto-update.ts
│   │   │       └── ui.ts
│   │   └── tsconfig.json
│   ├── 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
│   │   │   └── task-structure.mdx
│   │   ├── CHANGELOG.md
│   │   ├── docs.json
│   │   ├── favicon.svg
│   │   ├── getting-started
│   │   │   ├── 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
│   │   ├── 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
├── assets
│   ├── .windsurfrules
│   ├── AGENTS.md
│   ├── claude
│   │   ├── agents
│   │   │   ├── task-checker.md
│   │   │   ├── task-executor.md
│   │   │   └── task-orchestrator.md
│   │   ├── commands
│   │   │   └── tm
│   │   │       ├── add-dependency
│   │   │       │   └── add-dependency.md
│   │   │       ├── add-subtask
│   │   │       │   ├── add-subtask.md
│   │   │       │   └── convert-task-to-subtask.md
│   │   │       ├── add-task
│   │   │       │   └── add-task.md
│   │   │       ├── analyze-complexity
│   │   │       │   └── analyze-complexity.md
│   │   │       ├── clear-subtasks
│   │   │       │   ├── clear-all-subtasks.md
│   │   │       │   └── clear-subtasks.md
│   │   │       ├── complexity-report
│   │   │       │   └── complexity-report.md
│   │   │       ├── expand
│   │   │       │   ├── expand-all-tasks.md
│   │   │       │   └── expand-task.md
│   │   │       ├── fix-dependencies
│   │   │       │   └── fix-dependencies.md
│   │   │       ├── generate
│   │   │       │   └── generate-tasks.md
│   │   │       ├── help.md
│   │   │       ├── init
│   │   │       │   ├── init-project-quick.md
│   │   │       │   └── init-project.md
│   │   │       ├── learn.md
│   │   │       ├── list
│   │   │       │   ├── list-tasks-by-status.md
│   │   │       │   ├── list-tasks-with-subtasks.md
│   │   │       │   └── list-tasks.md
│   │   │       ├── models
│   │   │       │   ├── setup-models.md
│   │   │       │   └── view-models.md
│   │   │       ├── next
│   │   │       │   └── next-task.md
│   │   │       ├── parse-prd
│   │   │       │   ├── parse-prd-with-research.md
│   │   │       │   └── parse-prd.md
│   │   │       ├── remove-dependency
│   │   │       │   └── remove-dependency.md
│   │   │       ├── remove-subtask
│   │   │       │   └── remove-subtask.md
│   │   │       ├── remove-subtasks
│   │   │       │   ├── remove-all-subtasks.md
│   │   │       │   └── remove-subtasks.md
│   │   │       ├── remove-task
│   │   │       │   └── remove-task.md
│   │   │       ├── set-status
│   │   │       │   ├── to-cancelled.md
│   │   │       │   ├── to-deferred.md
│   │   │       │   ├── to-done.md
│   │   │       │   ├── to-in-progress.md
│   │   │       │   ├── to-pending.md
│   │   │       │   └── to-review.md
│   │   │       ├── setup
│   │   │       │   ├── install-taskmaster.md
│   │   │       │   └── quick-install-taskmaster.md
│   │   │       ├── show
│   │   │       │   └── show-task.md
│   │   │       ├── status
│   │   │       │   └── project-status.md
│   │   │       ├── sync-readme
│   │   │       │   └── sync-readme.md
│   │   │       ├── tm-main.md
│   │   │       ├── update
│   │   │       │   ├── update-single-task.md
│   │   │       │   ├── update-task.md
│   │   │       │   └── update-tasks-from-id.md
│   │   │       ├── utils
│   │   │       │   └── analyze-project.md
│   │   │       ├── validate-dependencies
│   │   │       │   └── validate-dependencies.md
│   │   │       └── workflows
│   │   │           ├── auto-implement-tasks.md
│   │   │           ├── command-pipeline.md
│   │   │           └── smart-workflow.md
│   │   └── TM_COMMANDS_GUIDE.md
│   ├── config.json
│   ├── env.example
│   ├── example_prd.txt
│   ├── 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.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
│   ├── CLI-COMMANDER-PATTERN.md
│   ├── command-reference.md
│   ├── configuration.md
│   ├── contributor-docs
│   │   └── testing-roo-integration.md
│   ├── cross-tag-task-movement.md
│   ├── examples
│   │   └── claude-code-usage.md
│   ├── examples.md
│   ├── licensing.md
│   ├── mcp-provider-guide.md
│   ├── mcp-provider.md
│   ├── migration-guide.md
│   ├── models.md
│   ├── providers
│   │   └── gemini-cli.md
│   ├── README.md
│   ├── scripts
│   │   └── models-json-to-markdown.js
│   ├── task-structure.md
│   └── tutorial.md
├── images
│   └── 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
│       │   │   ├── list-tasks.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
│       │   │   ├── show-task.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
│           ├── get-task.js
│           ├── get-tasks.js
│           ├── index.js
│           ├── initialize-project.js
│           ├── list-tags.js
│           ├── models.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.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
│   ├── build-config
│   │   ├── CHANGELOG.md
│   │   ├── package.json
│   │   ├── src
│   │   │   └── tsdown.base.ts
│   │   └── tsconfig.json
│   └── tm-core
│       ├── .gitignore
│       ├── CHANGELOG.md
│       ├── docs
│       │   └── listTasks-architecture.md
│       ├── package.json
│       ├── POC-STATUS.md
│       ├── README.md
│       ├── src
│       │   ├── auth
│       │   │   ├── auth-manager.test.ts
│       │   │   ├── auth-manager.ts
│       │   │   ├── config.ts
│       │   │   ├── credential-store.test.ts
│       │   │   ├── credential-store.ts
│       │   │   ├── index.ts
│       │   │   ├── oauth-service.ts
│       │   │   ├── supabase-session-storage.ts
│       │   │   └── types.ts
│       │   ├── clients
│       │   │   ├── index.ts
│       │   │   └── supabase-client.ts
│       │   ├── config
│       │   │   ├── config-manager.spec.ts
│       │   │   ├── config-manager.ts
│       │   │   ├── index.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
│       │   ├── constants
│       │   │   └── index.ts
│       │   ├── entities
│       │   │   └── task.entity.ts
│       │   ├── errors
│       │   │   ├── index.ts
│       │   │   └── task-master-error.ts
│       │   ├── executors
│       │   │   ├── base-executor.ts
│       │   │   ├── claude-executor.ts
│       │   │   ├── executor-factory.ts
│       │   │   ├── executor-service.ts
│       │   │   ├── index.ts
│       │   │   └── types.ts
│       │   ├── index.ts
│       │   ├── interfaces
│       │   │   ├── ai-provider.interface.ts
│       │   │   ├── configuration.interface.ts
│       │   │   ├── index.ts
│       │   │   └── storage.interface.ts
│       │   ├── logger
│       │   │   ├── factory.ts
│       │   │   ├── index.ts
│       │   │   └── logger.ts
│       │   ├── mappers
│       │   │   └── TaskMapper.ts
│       │   ├── parser
│       │   │   └── index.ts
│       │   ├── providers
│       │   │   ├── ai
│       │   │   │   ├── base-provider.ts
│       │   │   │   └── index.ts
│       │   │   └── index.ts
│       │   ├── repositories
│       │   │   ├── supabase-task-repository.ts
│       │   │   └── task-repository.interface.ts
│       │   ├── services
│       │   │   ├── index.ts
│       │   │   ├── organization.service.ts
│       │   │   ├── task-execution-service.ts
│       │   │   └── task-service.ts
│       │   ├── storage
│       │   │   ├── api-storage.ts
│       │   │   ├── file-storage
│       │   │   │   ├── file-operations.ts
│       │   │   │   ├── file-storage.ts
│       │   │   │   ├── format-handler.ts
│       │   │   │   ├── index.ts
│       │   │   │   └── path-resolver.ts
│       │   │   ├── index.ts
│       │   │   └── storage-factory.ts
│       │   ├── subpath-exports.test.ts
│       │   ├── task-master-core.ts
│       │   ├── types
│       │   │   ├── database.types.ts
│       │   │   ├── index.ts
│       │   │   └── legacy.ts
│       │   └── utils
│       │       ├── id-generator.ts
│       │       └── index.ts
│       ├── tests
│       │   ├── integration
│       │   │   └── list-tasks.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
│   ├── dev.js
│   ├── init.js
│   ├── modules
│   │   ├── ai-services-unified.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
├── src
│   ├── ai-providers
│   │   ├── anthropic.js
│   │   ├── azure.js
│   │   ├── base-provider.js
│   │   ├── bedrock.js
│   │   ├── claude-code.js
│   │   ├── custom-sdk
│   │   │   ├── claude-code
│   │   │   │   ├── errors.js
│   │   │   │   ├── index.js
│   │   │   │   ├── json-extractor.js
│   │   │   │   ├── language-model.js
│   │   │   │   ├── message-converter.js
│   │   │   │   └── types.js
│   │   │   └── grok-cli
│   │   │       ├── errors.js
│   │   │       ├── index.js
│   │   │       ├── json-extractor.js
│   │   │       ├── language-model.js
│   │   │       ├── message-converter.js
│   │   │       └── types.js
│   │   ├── gemini-cli.js
│   │   ├── google-vertex.js
│   │   ├── google.js
│   │   ├── grok-cli.js
│   │   ├── groq.js
│   │   ├── index.js
│   │   ├── ollama.js
│   │   ├── openai.js
│   │   ├── openrouter.js
│   │   ├── perplexity.js
│   │   └── xai.js
│   ├── constants
│   │   ├── commands.js
│   │   ├── paths.js
│   │   ├── profiles.js
│   │   ├── providers.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
│   ├── 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
│   ├── fixture
│   │   └── test-tasks.json
│   ├── fixtures
│   │   ├── .taskmasterconfig
│   │   ├── sample-claude-response.js
│   │   ├── sample-prd.txt
│   │   └── sample-tasks.js
│   ├── integration
│   │   ├── 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
│   ├── 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
│       │   ├── claude-code.test.js
│       │   ├── custom-sdk
│       │   │   └── claude-code
│       │   │       └── language-model.test.js
│       │   ├── gemini-cli.test.js
│       │   ├── mcp-components.test.js
│       │   └── openai.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
│       ├── 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
│       ├── providers
│       │   └── provider-registry.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
│       │       │   ├── move-task-cross-tag.test.js
│       │       │   ├── move-task.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
```

# Files

--------------------------------------------------------------------------------
/scripts/modules/task-manager/update-task-by-id.js:
--------------------------------------------------------------------------------

```javascript
  1 | import fs from 'fs';
  2 | import path from 'path';
  3 | import chalk from 'chalk';
  4 | import boxen from 'boxen';
  5 | import Table from 'cli-table3';
  6 | import { z } from 'zod'; // Keep Zod for post-parse validation
  7 | 
  8 | import {
  9 | 	log as consoleLog,
 10 | 	readJSON,
 11 | 	writeJSON,
 12 | 	truncate,
 13 | 	isSilentMode,
 14 | 	flattenTasksWithSubtasks,
 15 | 	findProjectRoot
 16 | } from '../utils.js';
 17 | 
 18 | import {
 19 | 	getStatusWithColor,
 20 | 	startLoadingIndicator,
 21 | 	stopLoadingIndicator,
 22 | 	displayAiUsageSummary
 23 | } from '../ui.js';
 24 | 
 25 | import { generateTextService } from '../ai-services-unified.js';
 26 | import {
 27 | 	getDebugFlag,
 28 | 	isApiKeySet,
 29 | 	hasCodebaseAnalysis
 30 | } from '../config-manager.js';
 31 | import { getPromptManager } from '../prompt-manager.js';
 32 | import { ContextGatherer } from '../utils/contextGatherer.js';
 33 | import { FuzzyTaskSearch } from '../utils/fuzzyTaskSearch.js';
 34 | 
 35 | // Zod schema for post-parsing validation of the updated task object
 36 | const updatedTaskSchema = z
 37 | 	.object({
 38 | 		id: z.number().int(),
 39 | 		title: z.string(), // Title should be preserved, but check it exists
 40 | 		description: z.string(),
 41 | 		status: z.string(),
 42 | 		dependencies: z.array(z.union([z.number().int(), z.string()])),
 43 | 		priority: z.string().nullable().default('medium'),
 44 | 		details: z.string().nullable().default(''),
 45 | 		testStrategy: z.string().nullable().default(''),
 46 | 		subtasks: z
 47 | 			.array(
 48 | 				z.object({
 49 | 					id: z
 50 | 						.number()
 51 | 						.int()
 52 | 						.positive()
 53 | 						.describe('Sequential subtask ID starting from 1'),
 54 | 					title: z.string(),
 55 | 					description: z.string(),
 56 | 					status: z.string(),
 57 | 					dependencies: z.array(z.number().int()).nullable().default([]),
 58 | 					details: z.string().nullable().default(''),
 59 | 					testStrategy: z.string().nullable().default('')
 60 | 				})
 61 | 			)
 62 | 			.nullable()
 63 | 			.default([])
 64 | 	})
 65 | 	.strip(); // Allows parsing even if AI adds extra fields, but validation focuses on schema
 66 | 
 67 | /**
 68 |  * Parses a single updated task object from AI's text response.
 69 |  * @param {string} text - Response text from AI.
 70 |  * @param {number} expectedTaskId - The ID of the task expected.
 71 |  * @param {Function | Object} logFn - Logging function or MCP logger.
 72 |  * @param {boolean} isMCP - Flag indicating MCP context.
 73 |  * @returns {Object} Parsed and validated task object.
 74 |  * @throws {Error} If parsing or validation fails.
 75 |  */
 76 | function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
 77 | 	// Report helper consistent with the established pattern
 78 | 	const report = (level, ...args) => {
 79 | 		if (isMCP) {
 80 | 			if (typeof logFn[level] === 'function') logFn[level](...args);
 81 | 			else logFn.info(...args);
 82 | 		} else if (!isSilentMode()) {
 83 | 			logFn(level, ...args);
 84 | 		}
 85 | 	};
 86 | 
 87 | 	report(
 88 | 		'info',
 89 | 		'Attempting to parse updated task object from text response...'
 90 | 	);
 91 | 	if (!text || text.trim() === '')
 92 | 		throw new Error('AI response text is empty.');
 93 | 
 94 | 	let cleanedResponse = text.trim();
 95 | 	const originalResponseForDebug = cleanedResponse;
 96 | 	let parseMethodUsed = 'raw'; // Keep track of which method worked
 97 | 
 98 | 	// --- NEW Step 1: Try extracting between {} first ---
 99 | 	const firstBraceIndex = cleanedResponse.indexOf('{');
100 | 	const lastBraceIndex = cleanedResponse.lastIndexOf('}');
101 | 	let potentialJsonFromBraces = null;
102 | 
103 | 	if (firstBraceIndex !== -1 && lastBraceIndex > firstBraceIndex) {
104 | 		potentialJsonFromBraces = cleanedResponse.substring(
105 | 			firstBraceIndex,
106 | 			lastBraceIndex + 1
107 | 		);
108 | 		if (potentialJsonFromBraces.length <= 2) {
109 | 			potentialJsonFromBraces = null; // Ignore empty braces {}
110 | 		}
111 | 	}
112 | 
113 | 	// If {} extraction yielded something, try parsing it immediately
114 | 	if (potentialJsonFromBraces) {
115 | 		try {
116 | 			const testParse = JSON.parse(potentialJsonFromBraces);
117 | 			// It worked! Use this as the primary cleaned response.
118 | 			cleanedResponse = potentialJsonFromBraces;
119 | 			parseMethodUsed = 'braces';
120 | 		} catch (e) {
121 | 			report(
122 | 				'info',
123 | 				'Content between {} looked promising but failed initial parse. Proceeding to other methods.'
124 | 			);
125 | 			// Reset cleanedResponse to original if brace parsing failed
126 | 			cleanedResponse = originalResponseForDebug;
127 | 		}
128 | 	}
129 | 
130 | 	// --- Step 2: If brace parsing didn't work or wasn't applicable, try code block extraction ---
131 | 	if (parseMethodUsed === 'raw') {
132 | 		const codeBlockMatch = cleanedResponse.match(
133 | 			/```(?:json|javascript)?\s*([\s\S]*?)\s*```/i
134 | 		);
135 | 		if (codeBlockMatch) {
136 | 			cleanedResponse = codeBlockMatch[1].trim();
137 | 			parseMethodUsed = 'codeblock';
138 | 			report('info', 'Extracted JSON content from Markdown code block.');
139 | 		} else {
140 | 			// --- Step 3: If code block failed, try stripping prefixes ---
141 | 			const commonPrefixes = [
142 | 				'json\n',
143 | 				'javascript\n'
144 | 				// ... other prefixes ...
145 | 			];
146 | 			let prefixFound = false;
147 | 			for (const prefix of commonPrefixes) {
148 | 				if (cleanedResponse.toLowerCase().startsWith(prefix)) {
149 | 					cleanedResponse = cleanedResponse.substring(prefix.length).trim();
150 | 					parseMethodUsed = 'prefix';
151 | 					report('info', `Stripped prefix: "${prefix.trim()}"`);
152 | 					prefixFound = true;
153 | 					break;
154 | 				}
155 | 			}
156 | 			if (!prefixFound) {
157 | 				report(
158 | 					'warn',
159 | 					'Response does not appear to contain {}, code block, or known prefix. Attempting raw parse.'
160 | 				);
161 | 			}
162 | 		}
163 | 	}
164 | 
165 | 	// --- Step 4: Attempt final parse ---
166 | 	let parsedTask;
167 | 	try {
168 | 		parsedTask = JSON.parse(cleanedResponse);
169 | 	} catch (parseError) {
170 | 		report('error', `Failed to parse JSON object: ${parseError.message}`);
171 | 		report(
172 | 			'error',
173 | 			`Problematic JSON string (first 500 chars): ${cleanedResponse.substring(0, 500)}`
174 | 		);
175 | 		report(
176 | 			'error',
177 | 			`Original Raw Response (first 500 chars): ${originalResponseForDebug.substring(0, 500)}`
178 | 		);
179 | 		throw new Error(
180 | 			`Failed to parse JSON response object: ${parseError.message}`
181 | 		);
182 | 	}
183 | 
184 | 	if (!parsedTask || typeof parsedTask !== 'object') {
185 | 		report(
186 | 			'error',
187 | 			`Parsed content is not an object. Type: ${typeof parsedTask}`
188 | 		);
189 | 		report(
190 | 			'error',
191 | 			`Parsed content sample: ${JSON.stringify(parsedTask).substring(0, 200)}`
192 | 		);
193 | 		throw new Error('Parsed AI response is not a valid JSON object.');
194 | 	}
195 | 
196 | 	// Preprocess the task to ensure subtasks have proper structure
197 | 	const preprocessedTask = {
198 | 		...parsedTask,
199 | 		status: parsedTask.status || 'pending',
200 | 		dependencies: Array.isArray(parsedTask.dependencies)
201 | 			? parsedTask.dependencies
202 | 			: [],
203 | 		details:
204 | 			typeof parsedTask.details === 'string'
205 | 				? parsedTask.details
206 | 				: String(parsedTask.details || ''),
207 | 		testStrategy:
208 | 			typeof parsedTask.testStrategy === 'string'
209 | 				? parsedTask.testStrategy
210 | 				: String(parsedTask.testStrategy || ''),
211 | 		// Ensure subtasks is an array and each subtask has required fields
212 | 		subtasks: Array.isArray(parsedTask.subtasks)
213 | 			? parsedTask.subtasks.map((subtask) => ({
214 | 					...subtask,
215 | 					title: subtask.title || '',
216 | 					description: subtask.description || '',
217 | 					status: subtask.status || 'pending',
218 | 					dependencies: Array.isArray(subtask.dependencies)
219 | 						? subtask.dependencies
220 | 						: [],
221 | 					details:
222 | 						typeof subtask.details === 'string'
223 | 							? subtask.details
224 | 							: String(subtask.details || ''),
225 | 					testStrategy:
226 | 						typeof subtask.testStrategy === 'string'
227 | 							? subtask.testStrategy
228 | 							: String(subtask.testStrategy || '')
229 | 				}))
230 | 			: []
231 | 	};
232 | 
233 | 	// Validate the parsed task object using Zod
234 | 	const validationResult = updatedTaskSchema.safeParse(preprocessedTask);
235 | 	if (!validationResult.success) {
236 | 		report('error', 'Parsed task object failed Zod validation.');
237 | 		validationResult.error.errors.forEach((err) => {
238 | 			report('error', `  - Field '${err.path.join('.')}': ${err.message}`);
239 | 		});
240 | 		throw new Error(
241 | 			`AI response failed task structure validation: ${validationResult.error.message}`
242 | 		);
243 | 	}
244 | 
245 | 	// Final check: ensure ID matches expected ID (AI might hallucinate)
246 | 	if (validationResult.data.id !== expectedTaskId) {
247 | 		report(
248 | 			'warn',
249 | 			`AI returned task with ID ${validationResult.data.id}, but expected ${expectedTaskId}. Overwriting ID.`
250 | 		);
251 | 		validationResult.data.id = expectedTaskId; // Enforce correct ID
252 | 	}
253 | 
254 | 	report('info', 'Successfully validated updated task structure.');
255 | 	return validationResult.data; // Return the validated task data
256 | }
257 | 
258 | /**
259 |  * Update a task by ID with new information using the unified AI service.
260 |  * @param {string} tasksPath - Path to the tasks.json file
261 |  * @param {number} taskId - ID of the task to update
262 |  * @param {string} prompt - Prompt for generating updated task information
263 |  * @param {boolean} [useResearch=false] - Whether to use the research AI role.
264 |  * @param {Object} context - Context object containing session and mcpLog.
265 |  * @param {Object} [context.session] - Session object from MCP server.
266 |  * @param {Object} [context.mcpLog] - MCP logger object.
267 |  * @param {string} [context.projectRoot] - Project root path.
268 |  * @param {string} [context.tag] - Tag for the task
269 |  * @param {string} [outputFormat='text'] - Output format ('text' or 'json').
270 |  * @param {boolean} [appendMode=false] - If true, append to details instead of full update.
271 |  * @returns {Promise<Object|null>} - The updated task or null if update failed.
272 |  */
273 | async function updateTaskById(
274 | 	tasksPath,
275 | 	taskId,
276 | 	prompt,
277 | 	useResearch = false,
278 | 	context = {},
279 | 	outputFormat = 'text',
280 | 	appendMode = false
281 | ) {
282 | 	const { session, mcpLog, projectRoot: providedProjectRoot, tag } = context;
283 | 	const logFn = mcpLog || consoleLog;
284 | 	const isMCP = !!mcpLog;
285 | 
286 | 	// Use report helper for logging
287 | 	const report = (level, ...args) => {
288 | 		if (isMCP) {
289 | 			if (typeof logFn[level] === 'function') logFn[level](...args);
290 | 			else logFn.info(...args);
291 | 		} else if (!isSilentMode()) {
292 | 			logFn(level, ...args);
293 | 		}
294 | 	};
295 | 
296 | 	try {
297 | 		report('info', `Updating single task ${taskId} with prompt: "${prompt}"`);
298 | 
299 | 		// --- Input Validations (Keep existing) ---
300 | 		if (!Number.isInteger(taskId) || taskId <= 0)
301 | 			throw new Error(
302 | 				`Invalid task ID: ${taskId}. Task ID must be a positive integer.`
303 | 			);
304 | 		if (!prompt || typeof prompt !== 'string' || prompt.trim() === '')
305 | 			throw new Error('Prompt cannot be empty.');
306 | 		if (useResearch && !isApiKeySet('perplexity', session)) {
307 | 			report(
308 | 				'warn',
309 | 				'Perplexity research requested but API key not set. Falling back.'
310 | 			);
311 | 			if (outputFormat === 'text')
312 | 				console.log(
313 | 					chalk.yellow('Perplexity AI not available. Falling back to main AI.')
314 | 				);
315 | 			useResearch = false;
316 | 		}
317 | 		if (!fs.existsSync(tasksPath))
318 | 			throw new Error(`Tasks file not found: ${tasksPath}`);
319 | 		// --- End Input Validations ---
320 | 
321 | 		// Determine project root
322 | 		const projectRoot = providedProjectRoot || findProjectRoot();
323 | 		if (!projectRoot) {
324 | 			throw new Error('Could not determine project root directory');
325 | 		}
326 | 
327 | 		// --- Task Loading and Status Check (Keep existing) ---
328 | 		const data = readJSON(tasksPath, projectRoot, tag);
329 | 		if (!data || !data.tasks)
330 | 			throw new Error(`No valid tasks found in ${tasksPath}.`);
331 | 		const taskIndex = data.tasks.findIndex((task) => task.id === taskId);
332 | 		if (taskIndex === -1) throw new Error(`Task with ID ${taskId} not found.`);
333 | 		const taskToUpdate = data.tasks[taskIndex];
334 | 		if (taskToUpdate.status === 'done' || taskToUpdate.status === 'completed') {
335 | 			report(
336 | 				'warn',
337 | 				`Task ${taskId} is already marked as done and cannot be updated`
338 | 			);
339 | 
340 | 			// Only show warning box for text output (CLI)
341 | 			if (outputFormat === 'text') {
342 | 				console.log(
343 | 					boxen(
344 | 						chalk.yellow(
345 | 							`Task ${taskId} is already marked as ${taskToUpdate.status} and cannot be updated.`
346 | 						) +
347 | 							'\n\n' +
348 | 							chalk.white(
349 | 								'Completed tasks are locked to maintain consistency. To modify a completed task, you must first:'
350 | 							) +
351 | 							'\n' +
352 | 							chalk.white(
353 | 								'1. Change its status to "pending" or "in-progress"'
354 | 							) +
355 | 							'\n' +
356 | 							chalk.white('2. Then run the update-task command'),
357 | 						{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
358 | 					)
359 | 				);
360 | 			}
361 | 			return null;
362 | 		}
363 | 		// --- End Task Loading ---
364 | 
365 | 		// --- Context Gathering ---
366 | 		let gatheredContext = '';
367 | 		try {
368 | 			const contextGatherer = new ContextGatherer(projectRoot, tag);
369 | 			const allTasksFlat = flattenTasksWithSubtasks(data.tasks);
370 | 			const fuzzySearch = new FuzzyTaskSearch(allTasksFlat, 'update-task');
371 | 			const searchQuery = `${taskToUpdate.title} ${taskToUpdate.description} ${prompt}`;
372 | 			const searchResults = fuzzySearch.findRelevantTasks(searchQuery, {
373 | 				maxResults: 5,
374 | 				includeSelf: true
375 | 			});
376 | 			const relevantTaskIds = fuzzySearch.getTaskIds(searchResults);
377 | 
378 | 			const finalTaskIds = [
379 | 				...new Set([taskId.toString(), ...relevantTaskIds])
380 | 			];
381 | 
382 | 			if (finalTaskIds.length > 0) {
383 | 				const contextResult = await contextGatherer.gather({
384 | 					tasks: finalTaskIds,
385 | 					format: 'research'
386 | 				});
387 | 				gatheredContext = contextResult.context || '';
388 | 			}
389 | 		} catch (contextError) {
390 | 			report('warn', `Could not gather context: ${contextError.message}`);
391 | 		}
392 | 		// --- End Context Gathering ---
393 | 
394 | 		// --- Display Task Info (CLI Only - Keep existing) ---
395 | 		if (outputFormat === 'text') {
396 | 			// Show the task that will be updated
397 | 			const table = new Table({
398 | 				head: [
399 | 					chalk.cyan.bold('ID'),
400 | 					chalk.cyan.bold('Title'),
401 | 					chalk.cyan.bold('Status')
402 | 				],
403 | 				colWidths: [5, 60, 10]
404 | 			});
405 | 
406 | 			table.push([
407 | 				taskToUpdate.id,
408 | 				truncate(taskToUpdate.title, 57),
409 | 				getStatusWithColor(taskToUpdate.status)
410 | 			]);
411 | 
412 | 			console.log(
413 | 				boxen(chalk.white.bold(`Updating Task #${taskId}`), {
414 | 					padding: 1,
415 | 					borderColor: 'blue',
416 | 					borderStyle: 'round',
417 | 					margin: { top: 1, bottom: 0 }
418 | 				})
419 | 			);
420 | 
421 | 			console.log(table.toString());
422 | 
423 | 			// Display a message about how completed subtasks are handled
424 | 			console.log(
425 | 				boxen(
426 | 					chalk.cyan.bold('How Completed Subtasks Are Handled:') +
427 | 						'\n\n' +
428 | 						chalk.white(
429 | 							'• Subtasks marked as "done" or "completed" will be preserved\n'
430 | 						) +
431 | 						chalk.white(
432 | 							'• New subtasks will build upon what has already been completed\n'
433 | 						) +
434 | 						chalk.white(
435 | 							'• If completed work needs revision, a new subtask will be created instead of modifying done items\n'
436 | 						) +
437 | 						chalk.white(
438 | 							'• This approach maintains a clear record of completed work and new requirements'
439 | 						),
440 | 					{
441 | 						padding: 1,
442 | 						borderColor: 'blue',
443 | 						borderStyle: 'round',
444 | 						margin: { top: 1, bottom: 1 }
445 | 					}
446 | 				)
447 | 			);
448 | 		}
449 | 
450 | 		// --- Build Prompts using PromptManager ---
451 | 		const promptManager = getPromptManager();
452 | 
453 | 		const promptParams = {
454 | 			task: taskToUpdate,
455 | 			taskJson: JSON.stringify(taskToUpdate, null, 2),
456 | 			updatePrompt: prompt,
457 | 			appendMode: appendMode,
458 | 			useResearch: useResearch,
459 | 			currentDetails: taskToUpdate.details || '(No existing details)',
460 | 			gatheredContext: gatheredContext || '',
461 | 			hasCodebaseAnalysis: hasCodebaseAnalysis(
462 | 				useResearch,
463 | 				projectRoot,
464 | 				session
465 | 			),
466 | 			projectRoot: projectRoot
467 | 		};
468 | 
469 | 		const variantKey = appendMode
470 | 			? 'append'
471 | 			: useResearch
472 | 				? 'research'
473 | 				: 'default';
474 | 
475 | 		report(
476 | 			'info',
477 | 			`Loading prompt template with variant: ${variantKey}, appendMode: ${appendMode}, useResearch: ${useResearch}`
478 | 		);
479 | 
480 | 		let systemPrompt;
481 | 		let userPrompt;
482 | 		try {
483 | 			const promptResult = await promptManager.loadPrompt(
484 | 				'update-task',
485 | 				promptParams,
486 | 				variantKey
487 | 			);
488 | 			report(
489 | 				'info',
490 | 				`Prompt result type: ${typeof promptResult}, keys: ${promptResult ? Object.keys(promptResult).join(', ') : 'null'}`
491 | 			);
492 | 
493 | 			// Extract prompts - loadPrompt returns { systemPrompt, userPrompt, metadata }
494 | 			systemPrompt = promptResult.systemPrompt;
495 | 			userPrompt = promptResult.userPrompt;
496 | 
497 | 			report(
498 | 				'info',
499 | 				`Loaded prompts - systemPrompt length: ${systemPrompt?.length}, userPrompt length: ${userPrompt?.length}`
500 | 			);
501 | 		} catch (error) {
502 | 			report('error', `Failed to load prompt template: ${error.message}`);
503 | 			throw new Error(`Failed to load prompt template: ${error.message}`);
504 | 		}
505 | 
506 | 		// If prompts are still not set, throw an error
507 | 		if (!systemPrompt || !userPrompt) {
508 | 			throw new Error(
509 | 				`Failed to load prompts: systemPrompt=${!!systemPrompt}, userPrompt=${!!userPrompt}`
510 | 			);
511 | 		}
512 | 		// --- End Build Prompts ---
513 | 
514 | 		let loadingIndicator = null;
515 | 		let aiServiceResponse = null;
516 | 
517 | 		if (!isMCP && outputFormat === 'text') {
518 | 			loadingIndicator = startLoadingIndicator(
519 | 				useResearch ? 'Updating task with research...\n' : 'Updating task...\n'
520 | 			);
521 | 		}
522 | 
523 | 		try {
524 | 			const serviceRole = useResearch ? 'research' : 'main';
525 | 			aiServiceResponse = await generateTextService({
526 | 				role: serviceRole,
527 | 				session: session,
528 | 				projectRoot: projectRoot,
529 | 				systemPrompt: systemPrompt,
530 | 				prompt: userPrompt,
531 | 				commandName: 'update-task',
532 | 				outputType: isMCP ? 'mcp' : 'cli'
533 | 			});
534 | 
535 | 			if (loadingIndicator)
536 | 				stopLoadingIndicator(loadingIndicator, 'AI update complete.');
537 | 
538 | 			if (appendMode) {
539 | 				// Append mode: handle as plain text
540 | 				const generatedContentString = aiServiceResponse.mainResult;
541 | 				let newlyAddedSnippet = '';
542 | 
543 | 				if (generatedContentString && generatedContentString.trim()) {
544 | 					const timestamp = new Date().toISOString();
545 | 					const formattedBlock = `<info added on ${timestamp}>\n${generatedContentString.trim()}\n</info added on ${timestamp}>`;
546 | 					newlyAddedSnippet = formattedBlock;
547 | 
548 | 					// Append to task details
549 | 					taskToUpdate.details =
550 | 						(taskToUpdate.details ? taskToUpdate.details + '\n' : '') +
551 | 						formattedBlock;
552 | 				} else {
553 | 					report(
554 | 						'warn',
555 | 						'AI response was empty or whitespace after trimming. Original details remain unchanged.'
556 | 					);
557 | 					newlyAddedSnippet = 'No new details were added by the AI.';
558 | 				}
559 | 
560 | 				// Update description with timestamp if prompt is short
561 | 				if (prompt.length < 100) {
562 | 					if (taskToUpdate.description) {
563 | 						taskToUpdate.description += ` [Updated: ${new Date().toLocaleDateString()}]`;
564 | 					}
565 | 				}
566 | 
567 | 				// Write the updated task back to file
568 | 				data.tasks[taskIndex] = taskToUpdate;
569 | 				writeJSON(tasksPath, data, projectRoot, tag);
570 | 				report('success', `Successfully appended to task ${taskId}`);
571 | 
572 | 				// Display success message for CLI
573 | 				if (outputFormat === 'text') {
574 | 					console.log(
575 | 						boxen(
576 | 							chalk.green(`Successfully appended to task #${taskId}`) +
577 | 								'\n\n' +
578 | 								chalk.white.bold('Title:') +
579 | 								' ' +
580 | 								taskToUpdate.title +
581 | 								'\n\n' +
582 | 								chalk.white.bold('Newly Added Content:') +
583 | 								'\n' +
584 | 								chalk.white(newlyAddedSnippet),
585 | 							{ padding: 1, borderColor: 'green', borderStyle: 'round' }
586 | 						)
587 | 					);
588 | 				}
589 | 
590 | 				// Display AI usage telemetry for CLI users
591 | 				if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
592 | 					displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
593 | 				}
594 | 
595 | 				// Return the updated task
596 | 				return {
597 | 					updatedTask: taskToUpdate,
598 | 					telemetryData: aiServiceResponse.telemetryData,
599 | 					tagInfo: aiServiceResponse.tagInfo
600 | 				};
601 | 			}
602 | 
603 | 			// Full update mode: Use mainResult (text) for parsing
604 | 			const updatedTask = parseUpdatedTaskFromText(
605 | 				aiServiceResponse.mainResult,
606 | 				taskId,
607 | 				logFn,
608 | 				isMCP
609 | 			);
610 | 
611 | 			// --- Task Validation/Correction (Keep existing logic) ---
612 | 			if (!updatedTask || typeof updatedTask !== 'object')
613 | 				throw new Error('Received invalid task object from AI.');
614 | 			if (!updatedTask.title || !updatedTask.description)
615 | 				throw new Error('Updated task missing required fields.');
616 | 			// Preserve ID if AI changed it
617 | 			if (updatedTask.id !== taskId) {
618 | 				report('warn', `AI changed task ID. Restoring original ID ${taskId}.`);
619 | 				updatedTask.id = taskId;
620 | 			}
621 | 			// Preserve status if AI changed it
622 | 			if (
623 | 				updatedTask.status !== taskToUpdate.status &&
624 | 				!prompt.toLowerCase().includes('status')
625 | 			) {
626 | 				report(
627 | 					'warn',
628 | 					`AI changed task status. Restoring original status '${taskToUpdate.status}'.`
629 | 				);
630 | 				updatedTask.status = taskToUpdate.status;
631 | 			}
632 | 			// Fix subtask IDs if they exist (ensure they are numeric and sequential)
633 | 			if (updatedTask.subtasks && Array.isArray(updatedTask.subtasks)) {
634 | 				let currentSubtaskId = 1;
635 | 				updatedTask.subtasks = updatedTask.subtasks.map((subtask) => {
636 | 					// Fix AI-generated subtask IDs that might be strings or use parent ID as prefix
637 | 					const correctedSubtask = {
638 | 						...subtask,
639 | 						id: currentSubtaskId, // Override AI-generated ID with correct sequential ID
640 | 						dependencies: Array.isArray(subtask.dependencies)
641 | 							? subtask.dependencies
642 | 									.map((dep) =>
643 | 										typeof dep === 'string' ? parseInt(dep, 10) : dep
644 | 									)
645 | 									.filter(
646 | 										(depId) =>
647 | 											!Number.isNaN(depId) &&
648 | 											depId >= 1 &&
649 | 											depId < currentSubtaskId
650 | 									)
651 | 							: [],
652 | 						status: subtask.status || 'pending'
653 | 					};
654 | 					currentSubtaskId++;
655 | 					return correctedSubtask;
656 | 				});
657 | 				report(
658 | 					'info',
659 | 					`Fixed ${updatedTask.subtasks.length} subtask IDs to be sequential numeric IDs.`
660 | 				);
661 | 			}
662 | 
663 | 			// Preserve completed subtasks (Keep existing logic)
664 | 			if (taskToUpdate.subtasks?.length > 0) {
665 | 				if (!updatedTask.subtasks) {
666 | 					report(
667 | 						'warn',
668 | 						'Subtasks removed by AI. Restoring original subtasks.'
669 | 					);
670 | 					updatedTask.subtasks = taskToUpdate.subtasks;
671 | 				} else {
672 | 					const completedOriginal = taskToUpdate.subtasks.filter(
673 | 						(st) => st.status === 'done' || st.status === 'completed'
674 | 					);
675 | 					completedOriginal.forEach((compSub) => {
676 | 						const updatedSub = updatedTask.subtasks.find(
677 | 							(st) => st.id === compSub.id
678 | 						);
679 | 						if (
680 | 							!updatedSub ||
681 | 							JSON.stringify(updatedSub) !== JSON.stringify(compSub)
682 | 						) {
683 | 							report(
684 | 								'warn',
685 | 								`Completed subtask ${compSub.id} was modified or removed. Restoring.`
686 | 							);
687 | 							// Remove potentially modified version
688 | 							updatedTask.subtasks = updatedTask.subtasks.filter(
689 | 								(st) => st.id !== compSub.id
690 | 							);
691 | 							// Add back original
692 | 							updatedTask.subtasks.push(compSub);
693 | 						}
694 | 					});
695 | 					// Deduplicate just in case
696 | 					const subtaskIds = new Set();
697 | 					updatedTask.subtasks = updatedTask.subtasks.filter((st) => {
698 | 						if (!subtaskIds.has(st.id)) {
699 | 							subtaskIds.add(st.id);
700 | 							return true;
701 | 						}
702 | 						report('warn', `Duplicate subtask ID ${st.id} removed.`);
703 | 						return false;
704 | 					});
705 | 				}
706 | 			}
707 | 			// --- End Task Validation/Correction ---
708 | 
709 | 			// --- Update Task Data (Keep existing) ---
710 | 			data.tasks[taskIndex] = updatedTask;
711 | 			// --- End Update Task Data ---
712 | 
713 | 			// --- Write File and Generate (Unchanged) ---
714 | 			writeJSON(tasksPath, data, projectRoot, tag);
715 | 			report('success', `Successfully updated task ${taskId}`);
716 | 			// await generateTaskFiles(tasksPath, path.dirname(tasksPath));
717 | 			// --- End Write File ---
718 | 
719 | 			// --- Display CLI Telemetry ---
720 | 			if (outputFormat === 'text' && aiServiceResponse.telemetryData) {
721 | 				displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); // <<< ADD display
722 | 			}
723 | 
724 | 			// --- Return Success with Telemetry ---
725 | 			return {
726 | 				updatedTask: updatedTask, // Return the updated task object
727 | 				telemetryData: aiServiceResponse.telemetryData, // <<< ADD telemetryData
728 | 				tagInfo: aiServiceResponse.tagInfo
729 | 			};
730 | 		} catch (error) {
731 | 			// Catch errors from generateTextService
732 | 			if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
733 | 			report('error', `Error during AI service call: ${error.message}`);
734 | 			if (error.message.includes('API key')) {
735 | 				report('error', 'Please ensure API keys are configured correctly.');
736 | 			}
737 | 			throw error; // Re-throw error
738 | 		}
739 | 	} catch (error) {
740 | 		// General error catch
741 | 		// --- General Error Handling (Keep existing) ---
742 | 		report('error', `Error updating task: ${error.message}`);
743 | 		if (outputFormat === 'text') {
744 | 			console.error(chalk.red(`Error: ${error.message}`));
745 | 			// ... helpful hints ...
746 | 			if (getDebugFlag(session)) console.error(error);
747 | 			process.exit(1);
748 | 		} else {
749 | 			throw error; // Re-throw for MCP
750 | 		}
751 | 		return null; // Indicate failure in CLI case if process doesn't exit
752 | 		// --- End General Error Handling ---
753 | 	}
754 | }
755 | 
756 | export default updateTaskById;
757 | 
```

--------------------------------------------------------------------------------
/tests/unit/dependency-manager.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * Dependency Manager module tests
  3 |  */
  4 | 
  5 | import { jest } from '@jest/globals';
  6 | import {
  7 | 	validateTaskDependencies,
  8 | 	isCircularDependency,
  9 | 	removeDuplicateDependencies,
 10 | 	cleanupSubtaskDependencies,
 11 | 	ensureAtLeastOneIndependentSubtask,
 12 | 	validateAndFixDependencies,
 13 | 	canMoveWithDependencies
 14 | } from '../../scripts/modules/dependency-manager.js';
 15 | import * as utils from '../../scripts/modules/utils.js';
 16 | import { sampleTasks } from '../fixtures/sample-tasks.js';
 17 | 
 18 | // Mock dependencies
 19 | jest.mock('path');
 20 | jest.mock('chalk', () => ({
 21 | 	green: jest.fn((text) => `<green>${text}</green>`),
 22 | 	yellow: jest.fn((text) => `<yellow>${text}</yellow>`),
 23 | 	red: jest.fn((text) => `<red>${text}</red>`),
 24 | 	cyan: jest.fn((text) => `<cyan>${text}</cyan>`),
 25 | 	bold: jest.fn((text) => `<bold>${text}</bold>`)
 26 | }));
 27 | 
 28 | jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`));
 29 | 
 30 | jest.mock('@anthropic-ai/sdk', () => ({
 31 | 	Anthropic: jest.fn().mockImplementation(() => ({}))
 32 | }));
 33 | 
 34 | // Mock utils module
 35 | const mockTaskExists = jest.fn();
 36 | const mockFormatTaskId = jest.fn();
 37 | const mockFindCycles = jest.fn();
 38 | const mockLog = jest.fn();
 39 | const mockReadJSON = jest.fn();
 40 | const mockWriteJSON = jest.fn();
 41 | 
 42 | jest.mock('../../scripts/modules/utils.js', () => ({
 43 | 	log: mockLog,
 44 | 	readJSON: mockReadJSON,
 45 | 	writeJSON: mockWriteJSON,
 46 | 	taskExists: mockTaskExists,
 47 | 	formatTaskId: mockFormatTaskId,
 48 | 	findCycles: mockFindCycles
 49 | }));
 50 | 
 51 | jest.mock('../../scripts/modules/ui.js', () => ({
 52 | 	displayBanner: jest.fn()
 53 | }));
 54 | 
 55 | jest.mock('../../scripts/modules/task-manager.js', () => ({
 56 | 	generateTaskFiles: jest.fn()
 57 | }));
 58 | 
 59 | // Create a path for test files
 60 | const TEST_TASKS_PATH = 'tests/fixture/test-tasks.json';
 61 | 
 62 | describe('Dependency Manager Module', () => {
 63 | 	beforeEach(() => {
 64 | 		jest.clearAllMocks();
 65 | 
 66 | 		// Set default implementations
 67 | 		mockTaskExists.mockImplementation((tasks, id) => {
 68 | 			if (Array.isArray(tasks)) {
 69 | 				if (typeof id === 'string' && id.includes('.')) {
 70 | 					const [taskId, subtaskId] = id.split('.').map(Number);
 71 | 					const task = tasks.find((t) => t.id === taskId);
 72 | 					return (
 73 | 						task &&
 74 | 						task.subtasks &&
 75 | 						task.subtasks.some((st) => st.id === subtaskId)
 76 | 					);
 77 | 				}
 78 | 				return tasks.some(
 79 | 					(task) => task.id === (typeof id === 'string' ? parseInt(id, 10) : id)
 80 | 				);
 81 | 			}
 82 | 			return false;
 83 | 		});
 84 | 
 85 | 		mockFormatTaskId.mockImplementation((id) => {
 86 | 			if (typeof id === 'string' && id.includes('.')) {
 87 | 				return id;
 88 | 			}
 89 | 			return parseInt(id, 10);
 90 | 		});
 91 | 
 92 | 		mockFindCycles.mockImplementation((tasks) => {
 93 | 			// Simplified cycle detection for testing
 94 | 			const dependencyMap = new Map();
 95 | 
 96 | 			// Build dependency map
 97 | 			tasks.forEach((task) => {
 98 | 				if (task.dependencies) {
 99 | 					dependencyMap.set(task.id, task.dependencies);
100 | 				}
101 | 			});
102 | 
103 | 			const visited = new Set();
104 | 			const recursionStack = new Set();
105 | 
106 | 			function dfs(taskId) {
107 | 				visited.add(taskId);
108 | 				recursionStack.add(taskId);
109 | 
110 | 				const dependencies = dependencyMap.get(taskId) || [];
111 | 				for (const depId of dependencies) {
112 | 					if (!visited.has(depId)) {
113 | 						if (dfs(depId)) return true;
114 | 					} else if (recursionStack.has(depId)) {
115 | 						return true;
116 | 					}
117 | 				}
118 | 
119 | 				recursionStack.delete(taskId);
120 | 				return false;
121 | 			}
122 | 
123 | 			// Check for cycles starting from each unvisited node
124 | 			for (const taskId of dependencyMap.keys()) {
125 | 				if (!visited.has(taskId)) {
126 | 					if (dfs(taskId)) return true;
127 | 				}
128 | 			}
129 | 
130 | 			return false;
131 | 		});
132 | 	});
133 | 
134 | 	describe('isCircularDependency function', () => {
135 | 		test('should detect a direct circular dependency', () => {
136 | 			const tasks = [
137 | 				{ id: 1, dependencies: [2] },
138 | 				{ id: 2, dependencies: [1] }
139 | 			];
140 | 
141 | 			const result = isCircularDependency(tasks, 1);
142 | 			expect(result).toBe(true);
143 | 		});
144 | 
145 | 		test('should detect an indirect circular dependency', () => {
146 | 			const tasks = [
147 | 				{ id: 1, dependencies: [2] },
148 | 				{ id: 2, dependencies: [3] },
149 | 				{ id: 3, dependencies: [1] }
150 | 			];
151 | 
152 | 			const result = isCircularDependency(tasks, 1);
153 | 			expect(result).toBe(true);
154 | 		});
155 | 
156 | 		test('should return false for non-circular dependencies', () => {
157 | 			const tasks = [
158 | 				{ id: 1, dependencies: [2] },
159 | 				{ id: 2, dependencies: [3] },
160 | 				{ id: 3, dependencies: [] }
161 | 			];
162 | 
163 | 			const result = isCircularDependency(tasks, 1);
164 | 			expect(result).toBe(false);
165 | 		});
166 | 
167 | 		test('should handle a task with no dependencies', () => {
168 | 			const tasks = [
169 | 				{ id: 1, dependencies: [] },
170 | 				{ id: 2, dependencies: [1] }
171 | 			];
172 | 
173 | 			const result = isCircularDependency(tasks, 1);
174 | 			expect(result).toBe(false);
175 | 		});
176 | 
177 | 		test('should handle a task depending on itself', () => {
178 | 			const tasks = [{ id: 1, dependencies: [1] }];
179 | 
180 | 			const result = isCircularDependency(tasks, 1);
181 | 			expect(result).toBe(true);
182 | 		});
183 | 
184 | 		test('should handle subtask dependencies correctly', () => {
185 | 			const tasks = [
186 | 				{
187 | 					id: 1,
188 | 					dependencies: [],
189 | 					subtasks: [
190 | 						{ id: 1, dependencies: ['1.2'] },
191 | 						{ id: 2, dependencies: ['1.3'] },
192 | 						{ id: 3, dependencies: ['1.1'] }
193 | 					]
194 | 				}
195 | 			];
196 | 
197 | 			// This creates a circular dependency: 1.1 -> 1.2 -> 1.3 -> 1.1
198 | 			const result = isCircularDependency(tasks, '1.1', ['1.3', '1.2']);
199 | 			expect(result).toBe(true);
200 | 		});
201 | 
202 | 		test('should allow non-circular subtask dependencies within same parent', () => {
203 | 			const tasks = [
204 | 				{
205 | 					id: 1,
206 | 					dependencies: [],
207 | 					subtasks: [
208 | 						{ id: 1, dependencies: [] },
209 | 						{ id: 2, dependencies: ['1.1'] },
210 | 						{ id: 3, dependencies: ['1.2'] }
211 | 					]
212 | 				}
213 | 			];
214 | 
215 | 			// This is a valid dependency chain: 1.3 -> 1.2 -> 1.1
216 | 			const result = isCircularDependency(tasks, '1.1', []);
217 | 			expect(result).toBe(false);
218 | 		});
219 | 
220 | 		test('should properly handle dependencies between subtasks of the same parent', () => {
221 | 			const tasks = [
222 | 				{
223 | 					id: 1,
224 | 					dependencies: [],
225 | 					subtasks: [
226 | 						{ id: 1, dependencies: [] },
227 | 						{ id: 2, dependencies: ['1.1'] },
228 | 						{ id: 3, dependencies: [] }
229 | 					]
230 | 				}
231 | 			];
232 | 
233 | 			// Check if adding a dependency from subtask 1.3 to 1.2 creates a circular dependency
234 | 			// This should be false as 1.3 -> 1.2 -> 1.1 is a valid chain
235 | 			mockTaskExists.mockImplementation(() => true);
236 | 			const result = isCircularDependency(tasks, '1.3', ['1.2']);
237 | 			expect(result).toBe(false);
238 | 		});
239 | 
240 | 		test('should correctly detect circular dependencies in subtasks of the same parent', () => {
241 | 			const tasks = [
242 | 				{
243 | 					id: 1,
244 | 					dependencies: [],
245 | 					subtasks: [
246 | 						{ id: 1, dependencies: ['1.3'] },
247 | 						{ id: 2, dependencies: ['1.1'] },
248 | 						{ id: 3, dependencies: ['1.2'] }
249 | 					]
250 | 				}
251 | 			];
252 | 
253 | 			// This creates a circular dependency: 1.1 -> 1.3 -> 1.2 -> 1.1
254 | 			mockTaskExists.mockImplementation(() => true);
255 | 			const result = isCircularDependency(tasks, '1.2', ['1.1']);
256 | 			expect(result).toBe(true);
257 | 		});
258 | 	});
259 | 
260 | 	describe('validateTaskDependencies function', () => {
261 | 		test('should detect missing dependencies', () => {
262 | 			const tasks = [
263 | 				{ id: 1, dependencies: [99] }, // 99 doesn't exist
264 | 				{ id: 2, dependencies: [1] }
265 | 			];
266 | 
267 | 			const result = validateTaskDependencies(tasks);
268 | 
269 | 			expect(result.valid).toBe(false);
270 | 			expect(result.issues.length).toBeGreaterThan(0);
271 | 			expect(result.issues[0].type).toBe('missing');
272 | 			expect(result.issues[0].taskId).toBe(1);
273 | 			expect(result.issues[0].dependencyId).toBe(99);
274 | 		});
275 | 
276 | 		test('should detect circular dependencies', () => {
277 | 			const tasks = [
278 | 				{ id: 1, dependencies: [2] },
279 | 				{ id: 2, dependencies: [1] }
280 | 			];
281 | 
282 | 			const result = validateTaskDependencies(tasks);
283 | 
284 | 			expect(result.valid).toBe(false);
285 | 			expect(result.issues.some((issue) => issue.type === 'circular')).toBe(
286 | 				true
287 | 			);
288 | 		});
289 | 
290 | 		test('should detect self-dependencies', () => {
291 | 			const tasks = [{ id: 1, dependencies: [1] }];
292 | 
293 | 			const result = validateTaskDependencies(tasks);
294 | 
295 | 			expect(result.valid).toBe(false);
296 | 			expect(
297 | 				result.issues.some(
298 | 					(issue) => issue.type === 'self' && issue.taskId === 1
299 | 				)
300 | 			).toBe(true);
301 | 		});
302 | 
303 | 		test('should return valid for correct dependencies', () => {
304 | 			const tasks = [
305 | 				{ id: 1, dependencies: [] },
306 | 				{ id: 2, dependencies: [1] },
307 | 				{ id: 3, dependencies: [1, 2] }
308 | 			];
309 | 
310 | 			const result = validateTaskDependencies(tasks);
311 | 
312 | 			expect(result.valid).toBe(true);
313 | 			expect(result.issues.length).toBe(0);
314 | 		});
315 | 
316 | 		test('should handle tasks with no dependencies property', () => {
317 | 			const tasks = [
318 | 				{ id: 1 }, // Missing dependencies property
319 | 				{ id: 2, dependencies: [1] }
320 | 			];
321 | 
322 | 			const result = validateTaskDependencies(tasks);
323 | 
324 | 			// Should be valid since a missing dependencies property is interpreted as an empty array
325 | 			expect(result.valid).toBe(true);
326 | 		});
327 | 
328 | 		test('should handle subtask dependencies correctly', () => {
329 | 			const tasks = [
330 | 				{
331 | 					id: 1,
332 | 					dependencies: [],
333 | 					subtasks: [
334 | 						{ id: 1, dependencies: [] },
335 | 						{ id: 2, dependencies: ['1.1'] }, // Valid - depends on another subtask
336 | 						{ id: 3, dependencies: ['1.2'] } // Valid - depends on another subtask
337 | 					]
338 | 				},
339 | 				{
340 | 					id: 2,
341 | 					dependencies: ['1.3'], // Valid - depends on a subtask from task 1
342 | 					subtasks: []
343 | 				}
344 | 			];
345 | 
346 | 			// Set up mock to handle subtask validation
347 | 			mockTaskExists.mockImplementation((tasks, id) => {
348 | 				if (typeof id === 'string' && id.includes('.')) {
349 | 					const [taskId, subtaskId] = id.split('.').map(Number);
350 | 					const task = tasks.find((t) => t.id === taskId);
351 | 					return (
352 | 						task &&
353 | 						task.subtasks &&
354 | 						task.subtasks.some((st) => st.id === subtaskId)
355 | 					);
356 | 				}
357 | 				return tasks.some((task) => task.id === parseInt(id, 10));
358 | 			});
359 | 
360 | 			const result = validateTaskDependencies(tasks);
361 | 
362 | 			expect(result.valid).toBe(true);
363 | 			expect(result.issues.length).toBe(0);
364 | 		});
365 | 
366 | 		test('should detect missing subtask dependencies', () => {
367 | 			const tasks = [
368 | 				{
369 | 					id: 1,
370 | 					dependencies: [],
371 | 					subtasks: [
372 | 						{ id: 1, dependencies: ['1.4'] }, // Invalid - subtask 4 doesn't exist
373 | 						{ id: 2, dependencies: ['2.1'] } // Invalid - task 2 has no subtasks
374 | 					]
375 | 				},
376 | 				{
377 | 					id: 2,
378 | 					dependencies: [],
379 | 					subtasks: []
380 | 				}
381 | 			];
382 | 
383 | 			// Mock taskExists to correctly identify missing subtasks
384 | 			mockTaskExists.mockImplementation((taskArray, depId) => {
385 | 				if (typeof depId === 'string' && depId === '1.4') {
386 | 					return false; // Subtask 1.4 doesn't exist
387 | 				}
388 | 				if (typeof depId === 'string' && depId === '2.1') {
389 | 					return false; // Subtask 2.1 doesn't exist
390 | 				}
391 | 				return true; // All other dependencies exist
392 | 			});
393 | 
394 | 			const result = validateTaskDependencies(tasks);
395 | 
396 | 			expect(result.valid).toBe(false);
397 | 			expect(result.issues.length).toBeGreaterThan(0);
398 | 			// Should detect missing subtask dependencies
399 | 			expect(
400 | 				result.issues.some(
401 | 					(issue) =>
402 | 						issue.type === 'missing' &&
403 | 						String(issue.taskId) === '1.1' &&
404 | 						String(issue.dependencyId) === '1.4'
405 | 				)
406 | 			).toBe(true);
407 | 		});
408 | 
409 | 		test('should detect circular dependencies between subtasks', () => {
410 | 			const tasks = [
411 | 				{
412 | 					id: 1,
413 | 					dependencies: [],
414 | 					subtasks: [
415 | 						{ id: 1, dependencies: ['1.2'] },
416 | 						{ id: 2, dependencies: ['1.1'] } // Creates a circular dependency with 1.1
417 | 					]
418 | 				}
419 | 			];
420 | 
421 | 			// Mock isCircularDependency for subtasks
422 | 			mockFindCycles.mockReturnValue(true);
423 | 
424 | 			const result = validateTaskDependencies(tasks);
425 | 
426 | 			expect(result.valid).toBe(false);
427 | 			expect(result.issues.some((issue) => issue.type === 'circular')).toBe(
428 | 				true
429 | 			);
430 | 		});
431 | 
432 | 		test('should properly validate dependencies between subtasks of the same parent', () => {
433 | 			const tasks = [
434 | 				{
435 | 					id: 23,
436 | 					dependencies: [],
437 | 					subtasks: [
438 | 						{ id: 8, dependencies: ['23.13'] },
439 | 						{ id: 10, dependencies: ['23.8'] },
440 | 						{ id: 13, dependencies: [] }
441 | 					]
442 | 				}
443 | 			];
444 | 
445 | 			// Mock taskExists to validate the subtask dependencies
446 | 			mockTaskExists.mockImplementation((taskArray, id) => {
447 | 				if (typeof id === 'string') {
448 | 					if (id === '23.8' || id === '23.10' || id === '23.13') {
449 | 						return true;
450 | 					}
451 | 				}
452 | 				return false;
453 | 			});
454 | 
455 | 			const result = validateTaskDependencies(tasks);
456 | 
457 | 			expect(result.valid).toBe(true);
458 | 			expect(result.issues.length).toBe(0);
459 | 		});
460 | 	});
461 | 
462 | 	describe('removeDuplicateDependencies function', () => {
463 | 		test('should remove duplicate dependencies from tasks', () => {
464 | 			const tasksData = {
465 | 				tasks: [
466 | 					{ id: 1, dependencies: [2, 2, 3, 3, 3] },
467 | 					{ id: 2, dependencies: [3] },
468 | 					{ id: 3, dependencies: [] }
469 | 				]
470 | 			};
471 | 
472 | 			const result = removeDuplicateDependencies(tasksData);
473 | 
474 | 			expect(result.tasks[0].dependencies).toEqual([2, 3]);
475 | 			expect(result.tasks[1].dependencies).toEqual([3]);
476 | 			expect(result.tasks[2].dependencies).toEqual([]);
477 | 		});
478 | 
479 | 		test('should handle empty dependencies array', () => {
480 | 			const tasksData = {
481 | 				tasks: [
482 | 					{ id: 1, dependencies: [] },
483 | 					{ id: 2, dependencies: [1] }
484 | 				]
485 | 			};
486 | 
487 | 			const result = removeDuplicateDependencies(tasksData);
488 | 
489 | 			expect(result.tasks[0].dependencies).toEqual([]);
490 | 			expect(result.tasks[1].dependencies).toEqual([1]);
491 | 		});
492 | 
493 | 		test('should handle tasks with no dependencies property', () => {
494 | 			const tasksData = {
495 | 				tasks: [
496 | 					{ id: 1 }, // No dependencies property
497 | 					{ id: 2, dependencies: [1] }
498 | 				]
499 | 			};
500 | 
501 | 			const result = removeDuplicateDependencies(tasksData);
502 | 
503 | 			expect(result.tasks[0]).not.toHaveProperty('dependencies');
504 | 			expect(result.tasks[1].dependencies).toEqual([1]);
505 | 		});
506 | 	});
507 | 
508 | 	describe('cleanupSubtaskDependencies function', () => {
509 | 		test('should remove dependencies to non-existent subtasks', () => {
510 | 			const tasksData = {
511 | 				tasks: [
512 | 					{
513 | 						id: 1,
514 | 						dependencies: [],
515 | 						subtasks: [
516 | 							{ id: 1, dependencies: [] },
517 | 							{ id: 2, dependencies: [3] } // Dependency 3 doesn't exist
518 | 						]
519 | 					},
520 | 					{
521 | 						id: 2,
522 | 						dependencies: ['1.2'], // Valid subtask dependency
523 | 						subtasks: [
524 | 							{ id: 1, dependencies: ['1.1'] } // Valid subtask dependency
525 | 						]
526 | 					}
527 | 				]
528 | 			};
529 | 
530 | 			const result = cleanupSubtaskDependencies(tasksData);
531 | 
532 | 			// Should remove the invalid dependency to subtask 3
533 | 			expect(result.tasks[0].subtasks[1].dependencies).toEqual([]);
534 | 			// Should keep valid dependencies
535 | 			expect(result.tasks[1].dependencies).toEqual(['1.2']);
536 | 			expect(result.tasks[1].subtasks[0].dependencies).toEqual(['1.1']);
537 | 		});
538 | 
539 | 		test('should handle tasks without subtasks', () => {
540 | 			const tasksData = {
541 | 				tasks: [
542 | 					{ id: 1, dependencies: [] },
543 | 					{ id: 2, dependencies: [1] }
544 | 				]
545 | 			};
546 | 
547 | 			const result = cleanupSubtaskDependencies(tasksData);
548 | 
549 | 			// Should return the original data unchanged
550 | 			expect(result).toEqual(tasksData);
551 | 		});
552 | 	});
553 | 
554 | 	describe('ensureAtLeastOneIndependentSubtask function', () => {
555 | 		test('should clear dependencies of first subtask if none are independent', () => {
556 | 			const tasksData = {
557 | 				tasks: [
558 | 					{
559 | 						id: 1,
560 | 						subtasks: [
561 | 							{ id: 1, dependencies: [2] },
562 | 							{ id: 2, dependencies: [1] }
563 | 						]
564 | 					}
565 | 				]
566 | 			};
567 | 
568 | 			const result = ensureAtLeastOneIndependentSubtask(tasksData);
569 | 
570 | 			expect(result).toBe(true);
571 | 			expect(tasksData.tasks[0].subtasks[0].dependencies).toEqual([]);
572 | 			expect(tasksData.tasks[0].subtasks[1].dependencies).toEqual([1]);
573 | 		});
574 | 
575 | 		test('should not modify tasks if at least one subtask is independent', () => {
576 | 			const tasksData = {
577 | 				tasks: [
578 | 					{
579 | 						id: 1,
580 | 						subtasks: [
581 | 							{ id: 1, dependencies: [] },
582 | 							{ id: 2, dependencies: [1] }
583 | 						]
584 | 					}
585 | 				]
586 | 			};
587 | 
588 | 			const result = ensureAtLeastOneIndependentSubtask(tasksData);
589 | 
590 | 			expect(result).toBe(false);
591 | 			expect(tasksData.tasks[0].subtasks[0].dependencies).toEqual([]);
592 | 			expect(tasksData.tasks[0].subtasks[1].dependencies).toEqual([1]);
593 | 		});
594 | 
595 | 		test('should handle tasks without subtasks', () => {
596 | 			const tasksData = {
597 | 				tasks: [{ id: 1 }, { id: 2, dependencies: [1] }]
598 | 			};
599 | 
600 | 			const result = ensureAtLeastOneIndependentSubtask(tasksData);
601 | 
602 | 			expect(result).toBe(false);
603 | 			expect(tasksData).toEqual({
604 | 				tasks: [{ id: 1 }, { id: 2, dependencies: [1] }]
605 | 			});
606 | 		});
607 | 
608 | 		test('should handle empty subtasks array', () => {
609 | 			const tasksData = {
610 | 				tasks: [{ id: 1, subtasks: [] }]
611 | 			};
612 | 
613 | 			const result = ensureAtLeastOneIndependentSubtask(tasksData);
614 | 
615 | 			expect(result).toBe(false);
616 | 			expect(tasksData).toEqual({
617 | 				tasks: [{ id: 1, subtasks: [] }]
618 | 			});
619 | 		});
620 | 	});
621 | 
622 | 	describe('validateAndFixDependencies function', () => {
623 | 		test('should fix multiple dependency issues and return true if changes made', () => {
624 | 			const tasksData = {
625 | 				tasks: [
626 | 					{
627 | 						id: 1,
628 | 						dependencies: [1, 1, 99], // Self-dependency and duplicate and invalid dependency
629 | 						subtasks: [
630 | 							{ id: 1, dependencies: [2, 2] }, // Duplicate dependencies
631 | 							{ id: 2, dependencies: [1] }
632 | 						]
633 | 					},
634 | 					{
635 | 						id: 2,
636 | 						dependencies: [1],
637 | 						subtasks: [
638 | 							{ id: 1, dependencies: [99] } // Invalid dependency
639 | 						]
640 | 					}
641 | 				]
642 | 			};
643 | 
644 | 			// Mock taskExists for validating dependencies
645 | 			mockTaskExists.mockImplementation((tasks, id) => {
646 | 				// Convert id to string for comparison
647 | 				const idStr = String(id);
648 | 
649 | 				// Handle subtask references (e.g., "1.2")
650 | 				if (idStr.includes('.')) {
651 | 					const [parentId, subtaskId] = idStr.split('.').map(Number);
652 | 					const task = tasks.find((t) => t.id === parentId);
653 | 					return (
654 | 						task &&
655 | 						task.subtasks &&
656 | 						task.subtasks.some((st) => st.id === subtaskId)
657 | 					);
658 | 				}
659 | 
660 | 				// Handle regular task references
661 | 				const taskId = parseInt(idStr, 10);
662 | 				return taskId === 1 || taskId === 2; // Only tasks 1 and 2 exist
663 | 			});
664 | 
665 | 			// Make a copy for verification that original is modified
666 | 			const originalData = JSON.parse(JSON.stringify(tasksData));
667 | 
668 | 			const result = validateAndFixDependencies(tasksData);
669 | 
670 | 			expect(result).toBe(true);
671 | 			// Check that data has been modified
672 | 			expect(tasksData).not.toEqual(originalData);
673 | 
674 | 			// Check specific changes
675 | 			// 1. Self-dependency removed
676 | 			expect(tasksData.tasks[0].dependencies).not.toContain(1);
677 | 			// 2. Invalid dependency removed
678 | 			expect(tasksData.tasks[0].dependencies).not.toContain(99);
679 | 			// 3. Dependencies have been deduplicated
680 | 			if (tasksData.tasks[0].subtasks[0].dependencies.length > 0) {
681 | 				expect(tasksData.tasks[0].subtasks[0].dependencies).toEqual(
682 | 					expect.arrayContaining([])
683 | 				);
684 | 			}
685 | 			// 4. Invalid subtask dependency removed
686 | 			expect(tasksData.tasks[1].subtasks[0].dependencies).toEqual([]);
687 | 
688 | 			// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
689 | 			expect(mockWriteJSON).not.toHaveBeenCalledWith(
690 | 				'tasks/tasks.json',
691 | 				expect.anything()
692 | 			);
693 | 		});
694 | 
695 | 		test('should return false if no changes needed', () => {
696 | 			const tasksData = {
697 | 				tasks: [
698 | 					{
699 | 						id: 1,
700 | 						dependencies: [],
701 | 						subtasks: [
702 | 							{ id: 1, dependencies: [] }, // Already has an independent subtask
703 | 							{ id: 2, dependencies: ['1.1'] }
704 | 						]
705 | 					},
706 | 					{
707 | 						id: 2,
708 | 						dependencies: [1]
709 | 					}
710 | 				]
711 | 			};
712 | 
713 | 			// Mock taskExists to validate all dependencies as valid
714 | 			mockTaskExists.mockImplementation((tasks, id) => {
715 | 				// Convert id to string for comparison
716 | 				const idStr = String(id);
717 | 
718 | 				// Handle subtask references
719 | 				if (idStr.includes('.')) {
720 | 					const [parentId, subtaskId] = idStr.split('.').map(Number);
721 | 					const task = tasks.find((t) => t.id === parentId);
722 | 					return (
723 | 						task &&
724 | 						task.subtasks &&
725 | 						task.subtasks.some((st) => st.id === subtaskId)
726 | 					);
727 | 				}
728 | 
729 | 				// Handle regular task references
730 | 				const taskId = parseInt(idStr, 10);
731 | 				return taskId === 1 || taskId === 2;
732 | 			});
733 | 
734 | 			const originalData = JSON.parse(JSON.stringify(tasksData));
735 | 			const result = validateAndFixDependencies(tasksData);
736 | 
737 | 			expect(result).toBe(false);
738 | 			// Verify data is unchanged
739 | 			expect(tasksData).toEqual(originalData);
740 | 
741 | 			// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
742 | 			expect(mockWriteJSON).not.toHaveBeenCalledWith(
743 | 				'tasks/tasks.json',
744 | 				expect.anything()
745 | 			);
746 | 		});
747 | 
748 | 		test('should handle invalid input', () => {
749 | 			expect(validateAndFixDependencies(null)).toBe(false);
750 | 			expect(validateAndFixDependencies({})).toBe(false);
751 | 			expect(validateAndFixDependencies({ tasks: null })).toBe(false);
752 | 			expect(validateAndFixDependencies({ tasks: 'not an array' })).toBe(false);
753 | 
754 | 			// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
755 | 			expect(mockWriteJSON).not.toHaveBeenCalledWith(
756 | 				'tasks/tasks.json',
757 | 				expect.anything()
758 | 			);
759 | 		});
760 | 
761 | 		test('should save changes when tasksPath is provided', () => {
762 | 			const tasksData = {
763 | 				tasks: [
764 | 					{
765 | 						id: 1,
766 | 						dependencies: [1, 1], // Self-dependency and duplicate
767 | 						subtasks: [
768 | 							{ id: 1, dependencies: [99] } // Invalid dependency
769 | 						]
770 | 					}
771 | 				]
772 | 			};
773 | 
774 | 			// Mock taskExists for this specific test
775 | 			mockTaskExists.mockImplementation((tasks, id) => {
776 | 				// Convert id to string for comparison
777 | 				const idStr = String(id);
778 | 
779 | 				// Handle subtask references
780 | 				if (idStr.includes('.')) {
781 | 					const [parentId, subtaskId] = idStr.split('.').map(Number);
782 | 					const task = tasks.find((t) => t.id === parentId);
783 | 					return (
784 | 						task &&
785 | 						task.subtasks &&
786 | 						task.subtasks.some((st) => st.id === subtaskId)
787 | 					);
788 | 				}
789 | 
790 | 				// Handle regular task references
791 | 				const taskId = parseInt(idStr, 10);
792 | 				return taskId === 1; // Only task 1 exists
793 | 			});
794 | 
795 | 			// Copy the original data to verify changes
796 | 			const originalData = JSON.parse(JSON.stringify(tasksData));
797 | 
798 | 			// Call the function with our test path instead of the actual tasks.json
799 | 			const result = validateAndFixDependencies(tasksData, TEST_TASKS_PATH);
800 | 
801 | 			// First verify that the result is true (changes were made)
802 | 			expect(result).toBe(true);
803 | 
804 | 			// Verify the data was modified
805 | 			expect(tasksData).not.toEqual(originalData);
806 | 
807 | 			// IMPORTANT: Verify no calls to writeJSON with actual tasks.json
808 | 			expect(mockWriteJSON).not.toHaveBeenCalledWith(
809 | 				'tasks/tasks.json',
810 | 				expect.anything()
811 | 			);
812 | 		});
813 | 	});
814 | 
815 | 	describe('canMoveWithDependencies', () => {
816 | 		it('should return canMove: false when conflicts exist', () => {
817 | 			const allTasks = [
818 | 				{
819 | 					id: 1,
820 | 					tag: 'source',
821 | 					dependencies: [2],
822 | 					title: 'Task 1'
823 | 				},
824 | 				{
825 | 					id: 2,
826 | 					tag: 'other',
827 | 					dependencies: [],
828 | 					title: 'Task 2'
829 | 				}
830 | 			];
831 | 
832 | 			const result = canMoveWithDependencies('1', 'source', 'target', allTasks);
833 | 
834 | 			expect(result.canMove).toBe(false);
835 | 			expect(result.conflicts).toBeDefined();
836 | 			expect(result.conflicts.length).toBeGreaterThan(0);
837 | 			expect(result.dependentTaskIds).toBeDefined();
838 | 		});
839 | 
840 | 		it('should return canMove: true when no conflicts exist', () => {
841 | 			const allTasks = [
842 | 				{
843 | 					id: 1,
844 | 					tag: 'source',
845 | 					dependencies: [],
846 | 					title: 'Task 1'
847 | 				},
848 | 				{
849 | 					id: 2,
850 | 					tag: 'target',
851 | 					dependencies: [],
852 | 					title: 'Task 2'
853 | 				}
854 | 			];
855 | 
856 | 			const result = canMoveWithDependencies('1', 'source', 'target', allTasks);
857 | 
858 | 			expect(result.canMove).toBe(true);
859 | 			expect(result.conflicts).toBeDefined();
860 | 			expect(result.conflicts.length).toBe(0);
861 | 			expect(result.dependentTaskIds).toBeDefined();
862 | 			expect(result.dependentTaskIds.length).toBe(0);
863 | 		});
864 | 
865 | 		it('should handle subtask lookup correctly', () => {
866 | 			const allTasks = [
867 | 				{
868 | 					id: 1,
869 | 					tag: 'source',
870 | 					dependencies: [],
871 | 					title: 'Parent Task',
872 | 					subtasks: [
873 | 						{
874 | 							id: 1,
875 | 							dependencies: [2],
876 | 							title: 'Subtask 1'
877 | 						}
878 | 					]
879 | 				},
880 | 				{
881 | 					id: 2,
882 | 					tag: 'other',
883 | 					dependencies: [],
884 | 					title: 'Task 2'
885 | 				}
886 | 			];
887 | 
888 | 			const result = canMoveWithDependencies(
889 | 				'1.1',
890 | 				'source',
891 | 				'target',
892 | 				allTasks
893 | 			);
894 | 
895 | 			expect(result.canMove).toBe(false);
896 | 			expect(result.conflicts).toBeDefined();
897 | 			expect(result.conflicts.length).toBeGreaterThan(0);
898 | 		});
899 | 
900 | 		it('should return error when task not found', () => {
901 | 			const allTasks = [
902 | 				{
903 | 					id: 1,
904 | 					tag: 'source',
905 | 					dependencies: [],
906 | 					title: 'Task 1'
907 | 				}
908 | 			];
909 | 
910 | 			const result = canMoveWithDependencies(
911 | 				'999',
912 | 				'source',
913 | 				'target',
914 | 				allTasks
915 | 			);
916 | 
917 | 			expect(result.canMove).toBe(false);
918 | 			expect(result.error).toBe('Task not found');
919 | 			expect(result.dependentTaskIds).toEqual([]);
920 | 			expect(result.conflicts).toEqual([]);
921 | 		});
922 | 	});
923 | });
924 | 
```

--------------------------------------------------------------------------------
/scripts/modules/task-manager/scope-adjustment.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * scope-adjustment.js
  3 |  * Core logic for dynamic task complexity adjustment (scope-up and scope-down)
  4 |  */
  5 | 
  6 | import { z } from 'zod';
  7 | import {
  8 | 	log,
  9 | 	readJSON,
 10 | 	writeJSON,
 11 | 	getCurrentTag,
 12 | 	readComplexityReport,
 13 | 	findTaskInComplexityReport
 14 | } from '../utils.js';
 15 | import {
 16 | 	generateObjectService,
 17 | 	generateTextService
 18 | } from '../ai-services-unified.js';
 19 | import { findTaskById, taskExists } from '../task-manager.js';
 20 | import analyzeTaskComplexity from './analyze-task-complexity.js';
 21 | import { findComplexityReportPath } from '../../../src/utils/path-utils.js';
 22 | 
 23 | /**
 24 |  * Valid strength levels for scope adjustments
 25 |  */
 26 | const VALID_STRENGTHS = ['light', 'regular', 'heavy'];
 27 | 
 28 | /**
 29 |  * Statuses that should be preserved during subtask regeneration
 30 |  * These represent work that has been started or intentionally set by the user
 31 |  */
 32 | const PRESERVE_STATUSES = [
 33 | 	'done',
 34 | 	'in-progress',
 35 | 	'review',
 36 | 	'cancelled',
 37 | 	'deferred',
 38 | 	'blocked'
 39 | ];
 40 | 
 41 | /**
 42 |  * Statuses that should be regenerated during subtask regeneration
 43 |  * These represent work that hasn't been started yet
 44 |  */
 45 | const REGENERATE_STATUSES = ['pending'];
 46 | 
 47 | /**
 48 |  * Validates strength parameter
 49 |  * @param {string} strength - The strength level to validate
 50 |  * @returns {boolean} True if valid, false otherwise
 51 |  */
 52 | export function validateStrength(strength) {
 53 | 	return VALID_STRENGTHS.includes(strength);
 54 | }
 55 | 
 56 | /**
 57 |  * Re-analyzes the complexity of a single task after scope adjustment
 58 |  * @param {Object} task - The task to analyze
 59 |  * @param {string} tasksPath - Path to tasks.json
 60 |  * @param {Object} context - Context containing projectRoot, tag, session
 61 |  * @returns {Promise<number|null>} New complexity score or null if analysis failed
 62 |  */
 63 | async function reanalyzeTaskComplexity(task, tasksPath, context) {
 64 | 	const { projectRoot, tag, session } = context;
 65 | 
 66 | 	try {
 67 | 		// Create a minimal tasks data structure for analysis
 68 | 		const tasksForAnalysis = {
 69 | 			tasks: [task],
 70 | 			metadata: { analyzedAt: new Date().toISOString() }
 71 | 		};
 72 | 
 73 | 		// Find the complexity report path for this tag
 74 | 		const complexityReportPath = findComplexityReportPath(
 75 | 			null,
 76 | 			{ projectRoot, tag },
 77 | 			null
 78 | 		);
 79 | 
 80 | 		if (!complexityReportPath) {
 81 | 			log('warn', 'No complexity report found - cannot re-analyze complexity');
 82 | 			return null;
 83 | 		}
 84 | 
 85 | 		// Use analyze-task-complexity to re-analyze just this task
 86 | 		const analysisOptions = {
 87 | 			file: tasksPath,
 88 | 			output: complexityReportPath,
 89 | 			id: task.id.toString(), // Analyze only this specific task
 90 | 			projectRoot,
 91 | 			tag,
 92 | 			_filteredTasksData: tasksForAnalysis, // Pass pre-filtered data
 93 | 			_originalTaskCount: 1
 94 | 		};
 95 | 
 96 | 		// Run the analysis with proper context
 97 | 		await analyzeTaskComplexity(analysisOptions, { session });
 98 | 
 99 | 		// Read the updated complexity report to get the new score
100 | 		const updatedReport = readComplexityReport(complexityReportPath);
101 | 		if (updatedReport) {
102 | 			const taskAnalysis = findTaskInComplexityReport(updatedReport, task.id);
103 | 			if (taskAnalysis) {
104 | 				log(
105 | 					'info',
106 | 					`Re-analyzed task ${task.id} complexity: ${taskAnalysis.complexityScore}/10`
107 | 				);
108 | 				return taskAnalysis.complexityScore;
109 | 			}
110 | 		}
111 | 
112 | 		log(
113 | 			'warn',
114 | 			`Could not find updated complexity analysis for task ${task.id}`
115 | 		);
116 | 		return null;
117 | 	} catch (error) {
118 | 		log('error', `Failed to re-analyze task complexity: ${error.message}`);
119 | 		return null;
120 | 	}
121 | }
122 | 
123 | /**
124 |  * Gets the current complexity score for a task from the complexity report
125 |  * @param {number} taskId - Task ID to look up
126 |  * @param {Object} context - Context containing projectRoot, tag
127 |  * @returns {number|null} Current complexity score or null if not found
128 |  */
129 | function getCurrentComplexityScore(taskId, context) {
130 | 	const { projectRoot, tag } = context;
131 | 
132 | 	try {
133 | 		// Find the complexity report path for this tag
134 | 		const complexityReportPath = findComplexityReportPath(
135 | 			null,
136 | 			{ projectRoot, tag },
137 | 			null
138 | 		);
139 | 
140 | 		if (!complexityReportPath) {
141 | 			return null;
142 | 		}
143 | 
144 | 		// Read the current complexity report
145 | 		const complexityReport = readComplexityReport(complexityReportPath);
146 | 		if (!complexityReport) {
147 | 			return null;
148 | 		}
149 | 
150 | 		// Find this task's current complexity
151 | 		const taskAnalysis = findTaskInComplexityReport(complexityReport, taskId);
152 | 		return taskAnalysis ? taskAnalysis.complexityScore : null;
153 | 	} catch (error) {
154 | 		log('debug', `Could not read current complexity score: ${error.message}`);
155 | 		return null;
156 | 	}
157 | }
158 | 
159 | /**
160 |  * Regenerates subtasks for a task based on new complexity while preserving completed work
161 |  * @param {Object} task - The updated task object
162 |  * @param {string} tasksPath - Path to tasks.json
163 |  * @param {Object} context - Context containing projectRoot, tag, session
164 |  * @param {string} direction - Direction of scope change (up/down) for logging
165 |  * @param {string} strength - Strength level ('light', 'regular', 'heavy')
166 |  * @param {number|null} originalComplexity - Original complexity score for smarter adjustments
167 |  * @returns {Promise<Object>} Object with updated task and regeneration info
168 |  */
169 | async function regenerateSubtasksForComplexity(
170 | 	task,
171 | 	tasksPath,
172 | 	context,
173 | 	direction,
174 | 	strength = 'regular',
175 | 	originalComplexity = null
176 | ) {
177 | 	const { projectRoot, tag, session } = context;
178 | 
179 | 	// Check if task has subtasks
180 | 	if (
181 | 		!task.subtasks ||
182 | 		!Array.isArray(task.subtasks) ||
183 | 		task.subtasks.length === 0
184 | 	) {
185 | 		return {
186 | 			updatedTask: task,
187 | 			regenerated: false,
188 | 			preserved: 0,
189 | 			generated: 0
190 | 		};
191 | 	}
192 | 
193 | 	// Identify subtasks to preserve vs regenerate
194 | 	const preservedSubtasks = task.subtasks.filter((subtask) =>
195 | 		PRESERVE_STATUSES.includes(subtask.status)
196 | 	);
197 | 	const pendingSubtasks = task.subtasks.filter((subtask) =>
198 | 		REGENERATE_STATUSES.includes(subtask.status)
199 | 	);
200 | 
201 | 	// If no pending subtasks, nothing to regenerate
202 | 	if (pendingSubtasks.length === 0) {
203 | 		return {
204 | 			updatedTask: task,
205 | 			regenerated: false,
206 | 			preserved: preservedSubtasks.length,
207 | 			generated: 0
208 | 		};
209 | 	}
210 | 
211 | 	// Calculate appropriate number of total subtasks based on direction, complexity, strength, and original complexity
212 | 	let targetSubtaskCount;
213 | 	const preservedCount = preservedSubtasks.length;
214 | 	const currentPendingCount = pendingSubtasks.length;
215 | 
216 | 	// Use original complexity to inform decisions (if available)
217 | 	const complexityFactor = originalComplexity
218 | 		? Math.max(0.5, originalComplexity / 10)
219 | 		: 1.0;
220 | 	const complexityInfo = originalComplexity
221 | 		? ` (original complexity: ${originalComplexity}/10)`
222 | 		: '';
223 | 
224 | 	if (direction === 'up') {
225 | 		// Scope up: More subtasks for increased complexity
226 | 		if (strength === 'light') {
227 | 			const base = Math.max(
228 | 				5,
229 | 				preservedCount + Math.ceil(currentPendingCount * 1.1)
230 | 			);
231 | 			targetSubtaskCount = Math.ceil(base * (0.8 + 0.4 * complexityFactor));
232 | 		} else if (strength === 'regular') {
233 | 			const base = Math.max(
234 | 				6,
235 | 				preservedCount + Math.ceil(currentPendingCount * 1.3)
236 | 			);
237 | 			targetSubtaskCount = Math.ceil(base * (0.8 + 0.4 * complexityFactor));
238 | 		} else {
239 | 			// heavy
240 | 			const base = Math.max(
241 | 				8,
242 | 				preservedCount + Math.ceil(currentPendingCount * 1.6)
243 | 			);
244 | 			targetSubtaskCount = Math.ceil(base * (0.8 + 0.6 * complexityFactor));
245 | 		}
246 | 	} else {
247 | 		// Scope down: Fewer subtasks for decreased complexity
248 | 		// High complexity tasks get reduced more aggressively
249 | 		const aggressiveFactor =
250 | 			originalComplexity >= 8 ? 0.7 : originalComplexity >= 6 ? 0.85 : 1.0;
251 | 
252 | 		if (strength === 'light') {
253 | 			const base = Math.max(
254 | 				3,
255 | 				preservedCount + Math.ceil(currentPendingCount * 0.8)
256 | 			);
257 | 			targetSubtaskCount = Math.ceil(base * aggressiveFactor);
258 | 		} else if (strength === 'regular') {
259 | 			const base = Math.max(
260 | 				3,
261 | 				preservedCount + Math.ceil(currentPendingCount * 0.5)
262 | 			);
263 | 			targetSubtaskCount = Math.ceil(base * aggressiveFactor);
264 | 		} else {
265 | 			// heavy
266 | 			// Heavy scope-down should be much more aggressive - aim for only core functionality
267 | 			// Very high complexity tasks (9-10) get reduced to almost nothing
268 | 			const ultraAggressiveFactor =
269 | 				originalComplexity >= 9 ? 0.3 : originalComplexity >= 7 ? 0.5 : 0.7;
270 | 			const base = Math.max(
271 | 				2,
272 | 				preservedCount + Math.ceil(currentPendingCount * 0.25)
273 | 			);
274 | 			targetSubtaskCount = Math.max(1, Math.ceil(base * ultraAggressiveFactor));
275 | 		}
276 | 	}
277 | 
278 | 	log(
279 | 		'debug',
280 | 		`Complexity-aware subtask calculation${complexityInfo}: ${currentPendingCount} pending -> target ${targetSubtaskCount} total`
281 | 	);
282 | 	log(
283 | 		'debug',
284 | 		`Complexity-aware calculation${complexityInfo}: ${currentPendingCount} pending -> ${targetSubtaskCount} total subtasks (${strength} ${direction})`
285 | 	);
286 | 
287 | 	const newSubtasksNeeded = Math.max(1, targetSubtaskCount - preservedCount);
288 | 
289 | 	try {
290 | 		// Generate new subtasks using AI to match the new complexity level
291 | 		const systemPrompt = `You are an expert project manager who creates task breakdowns that match complexity levels.`;
292 | 
293 | 		const prompt = `Based on this updated task, generate ${newSubtasksNeeded} NEW subtasks that reflect the ${direction === 'up' ? 'increased' : 'decreased'} complexity level:
294 | 
295 | **Task Title**: ${task.title}
296 | **Task Description**: ${task.description}
297 | **Implementation Details**: ${task.details}
298 | **Test Strategy**: ${task.testStrategy}
299 | 
300 | **Complexity Direction**: This task was recently scoped ${direction} (${strength} strength) to ${direction === 'up' ? 'increase' : 'decrease'} complexity.
301 | ${originalComplexity ? `**Original Complexity**: ${originalComplexity}/10 - consider this when determining appropriate scope level.` : ''}
302 | 
303 | ${preservedCount > 0 ? `**Preserved Subtasks**: ${preservedCount} existing subtasks with work already done will be kept.` : ''}
304 | 
305 | Generate subtasks that:
306 | ${
307 | 	direction === 'up'
308 | 		? strength === 'heavy'
309 | 			? `- Add comprehensive implementation steps with advanced features
310 | - Include extensive error handling, validation, and edge cases
311 | - Cover multiple integration scenarios and advanced testing
312 | - Provide thorough documentation and optimization approaches`
313 | 			: strength === 'regular'
314 | 				? `- Add more detailed implementation steps
315 | - Include additional error handling and validation
316 | - Cover more edge cases and advanced features
317 | - Provide more comprehensive testing approaches`
318 | 				: `- Add some additional implementation details
319 | - Include basic error handling considerations
320 | - Cover a few common edge cases
321 | - Enhance testing approaches slightly`
322 | 		: strength === 'heavy'
323 | 			? `- Focus ONLY on absolutely essential core functionality
324 | - Strip out ALL non-critical features (error handling, advanced testing, etc.)
325 | - Provide only the minimum viable implementation
326 | - Eliminate any complex integrations or advanced scenarios
327 | - Aim for the simplest possible working solution`
328 | 			: strength === 'regular'
329 | 				? `- Focus on core functionality only
330 | - Simplify implementation steps
331 | - Remove non-essential features
332 | - Streamline to basic requirements`
333 | 				: `- Focus mainly on core functionality
334 | - Slightly simplify implementation steps
335 | - Remove some non-essential features
336 | - Streamline most requirements`
337 | }
338 | 
339 | Return a JSON object with a "subtasks" array. Each subtask should have:
340 | - id: Sequential NUMBER starting from 1 (e.g., 1, 2, 3 - NOT "1", "2", "3")
341 | - title: Clear, specific title
342 | - description: Detailed description
343 | - dependencies: Array of dependency IDs as STRINGS (use format ["${task.id}.1", "${task.id}.2"] for siblings, or empty array [] for no dependencies)
344 | - details: Implementation guidance
345 | - status: "pending"
346 | - testStrategy: Testing approach
347 | 
348 | IMPORTANT: 
349 | - The 'id' field must be a NUMBER, not a string!
350 | - Dependencies must be strings, not numbers!
351 | 
352 | Ensure the JSON is valid and properly formatted.`;
353 | 
354 | 		// Define subtask schema
355 | 		const subtaskSchema = z.object({
356 | 			subtasks: z.array(
357 | 				z.object({
358 | 					id: z.number().int().positive(),
359 | 					title: z.string().min(5),
360 | 					description: z.string().min(10),
361 | 					dependencies: z.array(z.string()),
362 | 					details: z.string().min(20),
363 | 					status: z.string(),
364 | 					testStrategy: z.string()
365 | 				})
366 | 			)
367 | 		});
368 | 
369 | 		const aiResult = await generateObjectService({
370 | 			role: context.research ? 'research' : 'main',
371 | 			session: context.session,
372 | 			systemPrompt,
373 | 			prompt,
374 | 			schema: subtaskSchema,
375 | 			objectName: 'subtask_regeneration',
376 | 			commandName: context.commandName || `subtask-regen-${direction}`,
377 | 			outputType: context.outputType || 'cli'
378 | 		});
379 | 
380 | 		const generatedSubtasks = aiResult.mainResult.subtasks || [];
381 | 
382 | 		// Post-process generated subtasks to ensure defaults
383 | 		const processedGeneratedSubtasks = generatedSubtasks.map((subtask) => ({
384 | 			...subtask,
385 | 			status: subtask.status || 'pending',
386 | 			testStrategy: subtask.testStrategy || ''
387 | 		}));
388 | 
389 | 		// Update task with preserved subtasks + newly generated ones
390 | 		task.subtasks = [...preservedSubtasks, ...processedGeneratedSubtasks];
391 | 
392 | 		return {
393 | 			updatedTask: task,
394 | 			regenerated: true,
395 | 			preserved: preservedSubtasks.length,
396 | 			generated: processedGeneratedSubtasks.length
397 | 		};
398 | 	} catch (error) {
399 | 		log(
400 | 			'warn',
401 | 			`Failed to regenerate subtasks for task ${task.id}: ${error.message}`
402 | 		);
403 | 		// Don't fail the whole operation if subtask regeneration fails
404 | 		return {
405 | 			updatedTask: task,
406 | 			regenerated: false,
407 | 			preserved: preservedSubtasks.length,
408 | 			generated: 0,
409 | 			error: error.message
410 | 		};
411 | 	}
412 | }
413 | 
414 | /**
415 |  * Generates AI prompt for scope adjustment
416 |  * @param {Object} task - The task to adjust
417 |  * @param {string} direction - 'up' or 'down'
418 |  * @param {string} strength - 'light', 'regular', or 'heavy'
419 |  * @param {string} customPrompt - Optional custom instructions
420 |  * @returns {string} The generated prompt
421 |  */
422 | function generateScopePrompt(task, direction, strength, customPrompt) {
423 | 	const isUp = direction === 'up';
424 | 	const strengthDescriptions = {
425 | 		light: isUp ? 'minor enhancements' : 'slight simplifications',
426 | 		regular: isUp
427 | 			? 'moderate complexity increases'
428 | 			: 'moderate simplifications',
429 | 		heavy: isUp ? 'significant complexity additions' : 'major simplifications'
430 | 	};
431 | 
432 | 	let basePrompt = `You are tasked with adjusting the complexity of a task. 
433 | 
434 | CURRENT TASK:
435 | Title: ${task.title}
436 | Description: ${task.description}
437 | Details: ${task.details}
438 | Test Strategy: ${task.testStrategy || 'Not specified'}
439 | 
440 | ADJUSTMENT REQUIREMENTS:
441 | - Direction: ${isUp ? 'INCREASE' : 'DECREASE'} complexity
442 | - Strength: ${strength} (${strengthDescriptions[strength]})
443 | - Preserve the core purpose and functionality of the task
444 | - Maintain consistency with the existing task structure`;
445 | 
446 | 	if (isUp) {
447 | 		basePrompt += `
448 | - Add more detailed requirements, edge cases, or advanced features
449 | - Include additional implementation considerations
450 | - Enhance error handling and validation requirements
451 | - Expand testing strategies with more comprehensive scenarios`;
452 | 	} else {
453 | 		basePrompt += `
454 | - Focus on core functionality and essential requirements
455 | - Remove or simplify non-essential features  
456 | - Streamline implementation details
457 | - Simplify testing to focus on basic functionality`;
458 | 	}
459 | 
460 | 	if (customPrompt) {
461 | 		basePrompt += `\n\nCUSTOM INSTRUCTIONS:\n${customPrompt}`;
462 | 	}
463 | 
464 | 	basePrompt += `\n\nReturn a JSON object with the updated task containing these fields:
465 | - title: Updated task title
466 | - description: Updated task description  
467 | - details: Updated implementation details
468 | - testStrategy: Updated test strategy
469 | - priority: Task priority ('low', 'medium', or 'high')
470 | 
471 | Ensure the JSON is valid and properly formatted.`;
472 | 
473 | 	return basePrompt;
474 | }
475 | 
476 | /**
477 |  * Adjusts task complexity using AI
478 |  * @param {Object} task - The task to adjust
479 |  * @param {string} direction - 'up' or 'down'
480 |  * @param {string} strength - 'light', 'regular', or 'heavy'
481 |  * @param {string} customPrompt - Optional custom instructions
482 |  * @param {Object} context - Context object with projectRoot, tag, etc.
483 |  * @returns {Promise<Object>} Updated task data and telemetry
484 |  */
485 | async function adjustTaskComplexity(
486 | 	task,
487 | 	direction,
488 | 	strength,
489 | 	customPrompt,
490 | 	context
491 | ) {
492 | 	const systemPrompt = `You are an expert software project manager who helps adjust task complexity while maintaining clarity and actionability.`;
493 | 
494 | 	const prompt = generateScopePrompt(task, direction, strength, customPrompt);
495 | 
496 | 	// Define the task schema for structured response using Zod
497 | 	const taskSchema = z.object({
498 | 		title: z
499 | 			.string()
500 | 			.min(1)
501 | 			.describe('Updated task title reflecting scope adjustment'),
502 | 		description: z
503 | 			.string()
504 | 			.min(1)
505 | 			.describe('Updated task description with adjusted scope'),
506 | 		details: z
507 | 			.string()
508 | 			.min(1)
509 | 			.describe('Updated implementation details with adjusted complexity'),
510 | 		testStrategy: z
511 | 			.string()
512 | 			.min(1)
513 | 			.describe('Updated testing approach for the adjusted scope'),
514 | 		priority: z.enum(['low', 'medium', 'high']).describe('Task priority level')
515 | 	});
516 | 
517 | 	const aiResult = await generateObjectService({
518 | 		role: context.research ? 'research' : 'main',
519 | 		session: context.session,
520 | 		systemPrompt,
521 | 		prompt,
522 | 		schema: taskSchema,
523 | 		objectName: 'updated_task',
524 | 		commandName: context.commandName || `scope-${direction}`,
525 | 		outputType: context.outputType || 'cli'
526 | 	});
527 | 
528 | 	const updatedTaskData = aiResult.mainResult;
529 | 
530 | 	// Ensure priority has a value (in case AI didn't provide one)
531 | 	const processedTaskData = {
532 | 		...updatedTaskData,
533 | 		priority: updatedTaskData.priority || task.priority || 'medium'
534 | 	};
535 | 
536 | 	return {
537 | 		updatedTask: {
538 | 			...task,
539 | 			...processedTaskData
540 | 		},
541 | 		telemetryData: aiResult.telemetryData
542 | 	};
543 | }
544 | 
545 | /**
546 |  * Increases task complexity (scope-up)
547 |  * @param {string} tasksPath - Path to tasks.json file
548 |  * @param {Array<number>} taskIds - Array of task IDs to scope up
549 |  * @param {string} strength - Strength level ('light', 'regular', 'heavy')
550 |  * @param {string} customPrompt - Optional custom instructions
551 |  * @param {Object} context - Context object with projectRoot, tag, etc.
552 |  * @param {string} outputFormat - Output format ('text' or 'json')
553 |  * @returns {Promise<Object>} Results of the scope-up operation
554 |  */
555 | export async function scopeUpTask(
556 | 	tasksPath,
557 | 	taskIds,
558 | 	strength = 'regular',
559 | 	customPrompt = null,
560 | 	context = {},
561 | 	outputFormat = 'text'
562 | ) {
563 | 	// Validate inputs
564 | 	if (!validateStrength(strength)) {
565 | 		throw new Error(
566 | 			`Invalid strength level: ${strength}. Must be one of: ${VALID_STRENGTHS.join(', ')}`
567 | 		);
568 | 	}
569 | 
570 | 	const { projectRoot = '.', tag = 'master' } = context;
571 | 
572 | 	// Read tasks data
573 | 	const data = readJSON(tasksPath, projectRoot, tag);
574 | 	const tasks = data?.tasks || [];
575 | 
576 | 	// Validate all task IDs exist
577 | 	for (const taskId of taskIds) {
578 | 		if (!taskExists(tasks, taskId)) {
579 | 			throw new Error(`Task with ID ${taskId} not found`);
580 | 		}
581 | 	}
582 | 
583 | 	const updatedTasks = [];
584 | 	let combinedTelemetryData = null;
585 | 
586 | 	// Process each task
587 | 	for (const taskId of taskIds) {
588 | 		const taskResult = findTaskById(tasks, taskId);
589 | 		const task = taskResult.task;
590 | 		if (!task) {
591 | 			throw new Error(`Task with ID ${taskId} not found`);
592 | 		}
593 | 
594 | 		if (outputFormat === 'text') {
595 | 			log('info', `Scoping up task ${taskId}: ${task.title}`);
596 | 		}
597 | 
598 | 		// Get original complexity score (if available)
599 | 		const originalComplexity = getCurrentComplexityScore(taskId, context);
600 | 		if (originalComplexity && outputFormat === 'text') {
601 | 			log('info', `Original complexity: ${originalComplexity}/10`);
602 | 		}
603 | 
604 | 		const adjustResult = await adjustTaskComplexity(
605 | 			task,
606 | 			'up',
607 | 			strength,
608 | 			customPrompt,
609 | 			context
610 | 		);
611 | 
612 | 		// Regenerate subtasks based on new complexity while preserving completed work
613 | 		const subtaskResult = await regenerateSubtasksForComplexity(
614 | 			adjustResult.updatedTask,
615 | 			tasksPath,
616 | 			context,
617 | 			'up',
618 | 			strength,
619 | 			originalComplexity
620 | 		);
621 | 
622 | 		// Log subtask regeneration info if in text mode
623 | 		if (outputFormat === 'text' && subtaskResult.regenerated) {
624 | 			log(
625 | 				'info',
626 | 				`Regenerated ${subtaskResult.generated} pending subtasks (preserved ${subtaskResult.preserved} completed)`
627 | 			);
628 | 		}
629 | 
630 | 		// Update task in data
631 | 		const taskIndex = data.tasks.findIndex((t) => t.id === taskId);
632 | 		if (taskIndex !== -1) {
633 | 			data.tasks[taskIndex] = subtaskResult.updatedTask;
634 | 			updatedTasks.push(subtaskResult.updatedTask);
635 | 		}
636 | 
637 | 		// Re-analyze complexity after scoping (if we have a session for AI calls)
638 | 		if (context.session && originalComplexity) {
639 | 			try {
640 | 				// Write the updated task first so complexity analysis can read it
641 | 				writeJSON(tasksPath, data, projectRoot, tag);
642 | 
643 | 				// Re-analyze complexity
644 | 				const newComplexity = await reanalyzeTaskComplexity(
645 | 					subtaskResult.updatedTask,
646 | 					tasksPath,
647 | 					context
648 | 				);
649 | 				if (newComplexity && outputFormat === 'text') {
650 | 					const complexityChange = newComplexity - originalComplexity;
651 | 					const arrow =
652 | 						complexityChange > 0 ? '↗️' : complexityChange < 0 ? '↘️' : '➡️';
653 | 					log(
654 | 						'info',
655 | 						`New complexity: ${originalComplexity}/10 ${arrow} ${newComplexity}/10 (${complexityChange > 0 ? '+' : ''}${complexityChange})`
656 | 					);
657 | 				}
658 | 			} catch (error) {
659 | 				if (outputFormat === 'text') {
660 | 					log('warn', `Could not re-analyze complexity: ${error.message}`);
661 | 				}
662 | 			}
663 | 		}
664 | 
665 | 		// Combine telemetry data
666 | 		if (adjustResult.telemetryData) {
667 | 			if (!combinedTelemetryData) {
668 | 				combinedTelemetryData = { ...adjustResult.telemetryData };
669 | 			} else {
670 | 				// Sum up costs and tokens
671 | 				combinedTelemetryData.inputTokens +=
672 | 					adjustResult.telemetryData.inputTokens || 0;
673 | 				combinedTelemetryData.outputTokens +=
674 | 					adjustResult.telemetryData.outputTokens || 0;
675 | 				combinedTelemetryData.totalTokens +=
676 | 					adjustResult.telemetryData.totalTokens || 0;
677 | 				combinedTelemetryData.totalCost +=
678 | 					adjustResult.telemetryData.totalCost || 0;
679 | 			}
680 | 		}
681 | 	}
682 | 
683 | 	// Write updated data
684 | 	writeJSON(tasksPath, data, projectRoot, tag);
685 | 
686 | 	if (outputFormat === 'text') {
687 | 		log('info', `Successfully scoped up ${updatedTasks.length} task(s)`);
688 | 	}
689 | 
690 | 	return {
691 | 		updatedTasks,
692 | 		telemetryData: combinedTelemetryData
693 | 	};
694 | }
695 | 
696 | /**
697 |  * Decreases task complexity (scope-down)
698 |  * @param {string} tasksPath - Path to tasks.json file
699 |  * @param {Array<number>} taskIds - Array of task IDs to scope down
700 |  * @param {string} strength - Strength level ('light', 'regular', 'heavy')
701 |  * @param {string} customPrompt - Optional custom instructions
702 |  * @param {Object} context - Context object with projectRoot, tag, etc.
703 |  * @param {string} outputFormat - Output format ('text' or 'json')
704 |  * @returns {Promise<Object>} Results of the scope-down operation
705 |  */
706 | export async function scopeDownTask(
707 | 	tasksPath,
708 | 	taskIds,
709 | 	strength = 'regular',
710 | 	customPrompt = null,
711 | 	context = {},
712 | 	outputFormat = 'text'
713 | ) {
714 | 	// Validate inputs
715 | 	if (!validateStrength(strength)) {
716 | 		throw new Error(
717 | 			`Invalid strength level: ${strength}. Must be one of: ${VALID_STRENGTHS.join(', ')}`
718 | 		);
719 | 	}
720 | 
721 | 	const { projectRoot = '.', tag = 'master' } = context;
722 | 
723 | 	// Read tasks data
724 | 	const data = readJSON(tasksPath, projectRoot, tag);
725 | 	const tasks = data?.tasks || [];
726 | 
727 | 	// Validate all task IDs exist
728 | 	for (const taskId of taskIds) {
729 | 		if (!taskExists(tasks, taskId)) {
730 | 			throw new Error(`Task with ID ${taskId} not found`);
731 | 		}
732 | 	}
733 | 
734 | 	const updatedTasks = [];
735 | 	let combinedTelemetryData = null;
736 | 
737 | 	// Process each task
738 | 	for (const taskId of taskIds) {
739 | 		const taskResult = findTaskById(tasks, taskId);
740 | 		const task = taskResult.task;
741 | 		if (!task) {
742 | 			throw new Error(`Task with ID ${taskId} not found`);
743 | 		}
744 | 
745 | 		if (outputFormat === 'text') {
746 | 			log('info', `Scoping down task ${taskId}: ${task.title}`);
747 | 		}
748 | 
749 | 		// Get original complexity score (if available)
750 | 		const originalComplexity = getCurrentComplexityScore(taskId, context);
751 | 		if (originalComplexity && outputFormat === 'text') {
752 | 			log('info', `Original complexity: ${originalComplexity}/10`);
753 | 		}
754 | 
755 | 		const adjustResult = await adjustTaskComplexity(
756 | 			task,
757 | 			'down',
758 | 			strength,
759 | 			customPrompt,
760 | 			context
761 | 		);
762 | 
763 | 		// Regenerate subtasks based on new complexity while preserving completed work
764 | 		const subtaskResult = await regenerateSubtasksForComplexity(
765 | 			adjustResult.updatedTask,
766 | 			tasksPath,
767 | 			context,
768 | 			'down',
769 | 			strength,
770 | 			originalComplexity
771 | 		);
772 | 
773 | 		// Log subtask regeneration info if in text mode
774 | 		if (outputFormat === 'text' && subtaskResult.regenerated) {
775 | 			log(
776 | 				'info',
777 | 				`Regenerated ${subtaskResult.generated} pending subtasks (preserved ${subtaskResult.preserved} completed)`
778 | 			);
779 | 		}
780 | 
781 | 		// Update task in data
782 | 		const taskIndex = data.tasks.findIndex((t) => t.id === taskId);
783 | 		if (taskIndex !== -1) {
784 | 			data.tasks[taskIndex] = subtaskResult.updatedTask;
785 | 			updatedTasks.push(subtaskResult.updatedTask);
786 | 		}
787 | 
788 | 		// Re-analyze complexity after scoping (if we have a session for AI calls)
789 | 		if (context.session && originalComplexity) {
790 | 			try {
791 | 				// Write the updated task first so complexity analysis can read it
792 | 				writeJSON(tasksPath, data, projectRoot, tag);
793 | 
794 | 				// Re-analyze complexity
795 | 				const newComplexity = await reanalyzeTaskComplexity(
796 | 					subtaskResult.updatedTask,
797 | 					tasksPath,
798 | 					context
799 | 				);
800 | 				if (newComplexity && outputFormat === 'text') {
801 | 					const complexityChange = newComplexity - originalComplexity;
802 | 					const arrow =
803 | 						complexityChange > 0 ? '↗️' : complexityChange < 0 ? '↘️' : '➡️';
804 | 					log(
805 | 						'info',
806 | 						`New complexity: ${originalComplexity}/10 ${arrow} ${newComplexity}/10 (${complexityChange > 0 ? '+' : ''}${complexityChange})`
807 | 					);
808 | 				}
809 | 			} catch (error) {
810 | 				if (outputFormat === 'text') {
811 | 					log('warn', `Could not re-analyze complexity: ${error.message}`);
812 | 				}
813 | 			}
814 | 		}
815 | 
816 | 		// Combine telemetry data
817 | 		if (adjustResult.telemetryData) {
818 | 			if (!combinedTelemetryData) {
819 | 				combinedTelemetryData = { ...adjustResult.telemetryData };
820 | 			} else {
821 | 				// Sum up costs and tokens
822 | 				combinedTelemetryData.inputTokens +=
823 | 					adjustResult.telemetryData.inputTokens || 0;
824 | 				combinedTelemetryData.outputTokens +=
825 | 					adjustResult.telemetryData.outputTokens || 0;
826 | 				combinedTelemetryData.totalTokens +=
827 | 					adjustResult.telemetryData.totalTokens || 0;
828 | 				combinedTelemetryData.totalCost +=
829 | 					adjustResult.telemetryData.totalCost || 0;
830 | 			}
831 | 		}
832 | 	}
833 | 
834 | 	// Write updated data
835 | 	writeJSON(tasksPath, data, projectRoot, tag);
836 | 
837 | 	if (outputFormat === 'text') {
838 | 		log('info', `Successfully scoped down ${updatedTasks.length} task(s)`);
839 | 	}
840 | 
841 | 	return {
842 | 		updatedTasks,
843 | 		telemetryData: combinedTelemetryData
844 | 	};
845 | }
846 | 
```

--------------------------------------------------------------------------------
/scripts/modules/utils/contextGatherer.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * contextGatherer.js
  3 |  * Comprehensive context gathering utility for Task Master AI operations
  4 |  * Supports task context, file context, project tree, and custom context
  5 |  */
  6 | 
  7 | import fs from 'fs';
  8 | import path from 'path';
  9 | import pkg from 'gpt-tokens';
 10 | import Fuse from 'fuse.js';
 11 | import {
 12 | 	readJSON,
 13 | 	findTaskById,
 14 | 	truncate,
 15 | 	flattenTasksWithSubtasks
 16 | } from '../utils.js';
 17 | 
 18 | const { encode } = pkg;
 19 | 
 20 | /**
 21 |  * Context Gatherer class for collecting and formatting context from various sources
 22 |  */
 23 | export class ContextGatherer {
 24 | 	constructor(projectRoot, tag) {
 25 | 		this.projectRoot = projectRoot;
 26 | 		this.tasksPath = path.join(
 27 | 			projectRoot,
 28 | 			'.taskmaster',
 29 | 			'tasks',
 30 | 			'tasks.json'
 31 | 		);
 32 | 		this.tag = tag;
 33 | 		this.allTasks = this._loadAllTasks();
 34 | 	}
 35 | 
 36 | 	_loadAllTasks() {
 37 | 		try {
 38 | 			const data = readJSON(this.tasksPath, this.projectRoot, this.tag);
 39 | 			const tasks = data?.tasks || [];
 40 | 			return tasks;
 41 | 		} catch (error) {
 42 | 			console.warn(
 43 | 				`Warning: Could not load tasks for ContextGatherer: ${error.message}`
 44 | 			);
 45 | 			return [];
 46 | 		}
 47 | 	}
 48 | 
 49 | 	/**
 50 | 	 * Count tokens in a text string using gpt-tokens
 51 | 	 * @param {string} text - Text to count tokens for
 52 | 	 * @returns {number} Token count
 53 | 	 */
 54 | 	countTokens(text) {
 55 | 		if (!text || typeof text !== 'string') {
 56 | 			return 0;
 57 | 		}
 58 | 		try {
 59 | 			return encode(text).length;
 60 | 		} catch (error) {
 61 | 			// Fallback to rough character-based estimation if tokenizer fails
 62 | 			// Rough estimate: ~4 characters per token for English text
 63 | 			return Math.ceil(text.length / 4);
 64 | 		}
 65 | 	}
 66 | 
 67 | 	/**
 68 | 	 * Main method to gather context from multiple sources
 69 | 	 * @param {Object} options - Context gathering options
 70 | 	 * @param {Array<string>} [options.tasks] - Task/subtask IDs to include
 71 | 	 * @param {Array<string>} [options.files] - File paths to include
 72 | 	 * @param {string} [options.customContext] - Additional custom context
 73 | 	 * @param {boolean} [options.includeProjectTree] - Include project file tree
 74 | 	 * @param {string} [options.format] - Output format: 'research', 'chat', 'system-prompt'
 75 | 	 * @param {boolean} [options.includeTokenCounts] - Whether to include token breakdown
 76 | 	 * @param {string} [options.semanticQuery] - A query string for semantic task searching.
 77 | 	 * @param {number} [options.maxSemanticResults] - Max number of semantic results.
 78 | 	 * @param {Array<number>} [options.dependencyTasks] - Array of task IDs to build dependency graphs from.
 79 | 	 * @returns {Promise<Object>} Object with context string and analysis data
 80 | 	 */
 81 | 	async gather(options = {}) {
 82 | 		const {
 83 | 			tasks = [],
 84 | 			files = [],
 85 | 			customContext = '',
 86 | 			includeProjectTree = false,
 87 | 			format = 'research',
 88 | 			includeTokenCounts = false,
 89 | 			semanticQuery,
 90 | 			maxSemanticResults = 10,
 91 | 			dependencyTasks = []
 92 | 		} = options;
 93 | 
 94 | 		const contextSections = [];
 95 | 		const finalTaskIds = new Set(tasks.map(String));
 96 | 		let analysisData = null;
 97 | 		let tokenBreakdown = null;
 98 | 
 99 | 		// Initialize token breakdown if requested
100 | 		if (includeTokenCounts) {
101 | 			tokenBreakdown = {
102 | 				total: 0,
103 | 				customContext: null,
104 | 				tasks: [],
105 | 				files: [],
106 | 				projectTree: null
107 | 			};
108 | 		}
109 | 
110 | 		// Semantic Search
111 | 		if (semanticQuery && this.allTasks.length > 0) {
112 | 			const semanticResults = this._performSemanticSearch(
113 | 				semanticQuery,
114 | 				maxSemanticResults
115 | 			);
116 | 
117 | 			// Store the analysis data for UI display
118 | 			analysisData = semanticResults.analysisData;
119 | 
120 | 			semanticResults.tasks.forEach((task) => {
121 | 				finalTaskIds.add(String(task.id));
122 | 			});
123 | 		}
124 | 
125 | 		// Dependency Graph Analysis
126 | 		if (dependencyTasks.length > 0) {
127 | 			const dependencyResults = this._buildDependencyGraphs(dependencyTasks);
128 | 			dependencyResults.allRelatedTaskIds.forEach((id) =>
129 | 				finalTaskIds.add(String(id))
130 | 			);
131 | 			// We can format and add dependencyResults.graphVisualization later if needed
132 | 		}
133 | 
134 | 		// Add custom context first
135 | 		if (customContext && customContext.trim()) {
136 | 			const formattedCustomContext = this._formatCustomContext(
137 | 				customContext,
138 | 				format
139 | 			);
140 | 			contextSections.push(formattedCustomContext);
141 | 
142 | 			// Calculate tokens for custom context if requested
143 | 			if (includeTokenCounts) {
144 | 				tokenBreakdown.customContext = {
145 | 					tokens: this.countTokens(formattedCustomContext),
146 | 					characters: formattedCustomContext.length
147 | 				};
148 | 				tokenBreakdown.total += tokenBreakdown.customContext.tokens;
149 | 			}
150 | 		}
151 | 
152 | 		// Gather context for the final list of tasks
153 | 		if (finalTaskIds.size > 0) {
154 | 			const taskContextResult = await this._gatherTaskContext(
155 | 				Array.from(finalTaskIds),
156 | 				format,
157 | 				includeTokenCounts
158 | 			);
159 | 			if (taskContextResult.context) {
160 | 				contextSections.push(taskContextResult.context);
161 | 
162 | 				// Add task breakdown if token counting is enabled
163 | 				if (includeTokenCounts && taskContextResult.breakdown) {
164 | 					tokenBreakdown.tasks = taskContextResult.breakdown;
165 | 					const taskTokens = taskContextResult.breakdown.reduce(
166 | 						(sum, task) => sum + task.tokens,
167 | 						0
168 | 					);
169 | 					tokenBreakdown.total += taskTokens;
170 | 				}
171 | 			}
172 | 		}
173 | 
174 | 		// Add file context
175 | 		if (files.length > 0) {
176 | 			const fileContextResult = await this._gatherFileContext(
177 | 				files,
178 | 				format,
179 | 				includeTokenCounts
180 | 			);
181 | 			if (fileContextResult.context) {
182 | 				contextSections.push(fileContextResult.context);
183 | 
184 | 				// Add file breakdown if token counting is enabled
185 | 				if (includeTokenCounts && fileContextResult.breakdown) {
186 | 					tokenBreakdown.files = fileContextResult.breakdown;
187 | 					const fileTokens = fileContextResult.breakdown.reduce(
188 | 						(sum, file) => sum + file.tokens,
189 | 						0
190 | 					);
191 | 					tokenBreakdown.total += fileTokens;
192 | 				}
193 | 			}
194 | 		}
195 | 
196 | 		// Add project tree context
197 | 		if (includeProjectTree) {
198 | 			const treeContextResult = await this._gatherProjectTreeContext(
199 | 				format,
200 | 				includeTokenCounts
201 | 			);
202 | 			if (treeContextResult.context) {
203 | 				contextSections.push(treeContextResult.context);
204 | 
205 | 				// Add tree breakdown if token counting is enabled
206 | 				if (includeTokenCounts && treeContextResult.breakdown) {
207 | 					tokenBreakdown.projectTree = treeContextResult.breakdown;
208 | 					tokenBreakdown.total += treeContextResult.breakdown.tokens;
209 | 				}
210 | 			}
211 | 		}
212 | 
213 | 		const finalContext = this._joinContextSections(contextSections, format);
214 | 
215 | 		const result = {
216 | 			context: finalContext,
217 | 			analysisData: analysisData,
218 | 			contextSections: contextSections.length,
219 | 			finalTaskIds: Array.from(finalTaskIds)
220 | 		};
221 | 
222 | 		// Only include tokenBreakdown if it was requested
223 | 		if (includeTokenCounts) {
224 | 			result.tokenBreakdown = tokenBreakdown;
225 | 		}
226 | 
227 | 		return result;
228 | 	}
229 | 
230 | 	_performSemanticSearch(query, maxResults) {
231 | 		const searchableTasks = this.allTasks.map((task) => {
232 | 			const dependencyTitles =
233 | 				task.dependencies?.length > 0
234 | 					? task.dependencies
235 | 							.map((depId) => this.allTasks.find((t) => t.id === depId)?.title)
236 | 							.filter(Boolean)
237 | 							.join(' ')
238 | 					: '';
239 | 			return { ...task, dependencyTitles };
240 | 		});
241 | 
242 | 		// Use the exact same approach as add-task.js
243 | 		const searchOptions = {
244 | 			includeScore: true, // Return match scores
245 | 			threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
246 | 			keys: [
247 | 				{ name: 'title', weight: 1.5 }, // Title is most important
248 | 				{ name: 'description', weight: 2 }, // Description is very important
249 | 				{ name: 'details', weight: 3 }, // Details is most important
250 | 				// Search dependencies to find tasks that depend on similar things
251 | 				{ name: 'dependencyTitles', weight: 0.5 }
252 | 			],
253 | 			// Sort matches by score (lower is better)
254 | 			shouldSort: true,
255 | 			// Allow searching in nested properties
256 | 			useExtendedSearch: true,
257 | 			// Return up to 50 matches
258 | 			limit: 50
259 | 		};
260 | 
261 | 		// Create search index using Fuse.js
262 | 		const fuse = new Fuse(searchableTasks, searchOptions);
263 | 
264 | 		// Extract significant words and phrases from the prompt (like add-task.js does)
265 | 		const promptWords = query
266 | 			.toLowerCase()
267 | 			.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
268 | 			.split(/\s+/)
269 | 			.filter((word) => word.length > 3); // Words at least 4 chars
270 | 
271 | 		// Use the user's prompt for fuzzy search
272 | 		const fuzzyResults = fuse.search(query);
273 | 
274 | 		// Also search for each significant word to catch different aspects
275 | 		const wordResults = [];
276 | 		for (const word of promptWords) {
277 | 			if (word.length > 5) {
278 | 				// Only use significant words
279 | 				const results = fuse.search(word);
280 | 				if (results.length > 0) {
281 | 					wordResults.push(...results);
282 | 				}
283 | 			}
284 | 		}
285 | 
286 | 		// Merge and deduplicate results
287 | 		const mergedResults = [...fuzzyResults];
288 | 
289 | 		// Add word results that aren't already in fuzzyResults
290 | 		for (const wordResult of wordResults) {
291 | 			if (!mergedResults.some((r) => r.item.id === wordResult.item.id)) {
292 | 				mergedResults.push(wordResult);
293 | 			}
294 | 		}
295 | 
296 | 		// Group search results by relevance
297 | 		const highRelevance = mergedResults
298 | 			.filter((result) => result.score < 0.25)
299 | 			.map((result) => result.item);
300 | 
301 | 		const mediumRelevance = mergedResults
302 | 			.filter((result) => result.score >= 0.25 && result.score < 0.4)
303 | 			.map((result) => result.item);
304 | 
305 | 		// Get recent tasks (newest first)
306 | 		const recentTasks = [...this.allTasks]
307 | 			.sort((a, b) => b.id - a.id)
308 | 			.slice(0, 5);
309 | 
310 | 		// Combine high relevance, medium relevance, and recent tasks
311 | 		// Prioritize high relevance first
312 | 		const allRelevantTasks = [...highRelevance];
313 | 
314 | 		// Add medium relevance if not already included
315 | 		for (const task of mediumRelevance) {
316 | 			if (!allRelevantTasks.some((t) => t.id === task.id)) {
317 | 				allRelevantTasks.push(task);
318 | 			}
319 | 		}
320 | 
321 | 		// Add recent tasks if not already included
322 | 		for (const task of recentTasks) {
323 | 			if (!allRelevantTasks.some((t) => t.id === task.id)) {
324 | 				allRelevantTasks.push(task);
325 | 			}
326 | 		}
327 | 
328 | 		// Get top N results for context
329 | 		const finalResults = allRelevantTasks.slice(0, maxResults);
330 | 		return {
331 | 			tasks: finalResults,
332 | 			analysisData: {
333 | 				highRelevance: highRelevance,
334 | 				mediumRelevance: mediumRelevance,
335 | 				recentTasks: recentTasks,
336 | 				allRelevantTasks: allRelevantTasks
337 | 			}
338 | 		};
339 | 	}
340 | 
341 | 	_buildDependencyContext(taskIds) {
342 | 		const { allRelatedTaskIds, graphs, depthMap } =
343 | 			this._buildDependencyGraphs(taskIds);
344 | 		if (allRelatedTaskIds.size === 0) return '';
345 | 
346 | 		const dependentTasks = Array.from(allRelatedTaskIds)
347 | 			.map((id) => this.allTasks.find((t) => t.id === id))
348 | 			.filter(Boolean)
349 | 			.sort((a, b) => (depthMap.get(a.id) || 0) - (depthMap.get(b.id) || 0));
350 | 
351 | 		const uniqueDetailedTasks = dependentTasks.slice(0, 8);
352 | 
353 | 		let context = `\nThis task relates to a dependency structure with ${dependentTasks.length} related tasks in the chain.`;
354 | 
355 | 		const directDeps = this.allTasks.filter((t) => taskIds.includes(t.id));
356 | 		if (directDeps.length > 0) {
357 | 			context += `\n\nDirect dependencies:\n${directDeps
358 | 				.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
359 | 				.join('\n')}`;
360 | 		}
361 | 
362 | 		const indirectDeps = dependentTasks.filter((t) => !taskIds.includes(t.id));
363 | 		if (indirectDeps.length > 0) {
364 | 			context += `\n\nIndirect dependencies (dependencies of dependencies):\n${indirectDeps
365 | 				.slice(0, 5)
366 | 				.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
367 | 				.join('\n')}`;
368 | 			if (indirectDeps.length > 5)
369 | 				context += `\n- ... and ${
370 | 					indirectDeps.length - 5
371 | 				} more indirect dependencies`;
372 | 		}
373 | 
374 | 		context += `\n\nDetailed information about dependencies:`;
375 | 		for (const depTask of uniqueDetailedTasks) {
376 | 			const isDirect = taskIds.includes(depTask.id)
377 | 				? ' [DIRECT DEPENDENCY]'
378 | 				: '';
379 | 			context += `\n\n------ Task ${depTask.id}${isDirect}: ${depTask.title} ------\n`;
380 | 			context += `Description: ${depTask.description}\n`;
381 | 			if (depTask.dependencies?.length) {
382 | 				context += `Dependencies: ${depTask.dependencies.join(', ')}\n`;
383 | 			}
384 | 			if (depTask.details) {
385 | 				context += `Implementation Details: ${truncate(
386 | 					depTask.details,
387 | 					400
388 | 				)}\n`;
389 | 			}
390 | 		}
391 | 
392 | 		if (graphs.length > 0) {
393 | 			context += '\n\nDependency Chain Visualization:';
394 | 			context += graphs
395 | 				.map((graph) => this._formatDependencyChain(graph))
396 | 				.join('');
397 | 		}
398 | 
399 | 		return context;
400 | 	}
401 | 
402 | 	_buildDependencyGraphs(taskIds) {
403 | 		const visited = new Set();
404 | 		const depthMap = new Map();
405 | 		const graphs = [];
406 | 
407 | 		for (const id of taskIds) {
408 | 			const graph = this._buildDependencyGraph(id, visited, depthMap);
409 | 			if (graph) graphs.push(graph);
410 | 		}
411 | 
412 | 		return { allRelatedTaskIds: visited, graphs, depthMap };
413 | 	}
414 | 
415 | 	_buildDependencyGraph(taskId, visited, depthMap, depth = 0) {
416 | 		if (visited.has(taskId) || depth > 5) return null; // Limit recursion depth
417 | 		const task = this.allTasks.find((t) => t.id === taskId);
418 | 		if (!task) return null;
419 | 
420 | 		visited.add(taskId);
421 | 		if (!depthMap.has(taskId) || depth < depthMap.get(taskId)) {
422 | 			depthMap.set(taskId, depth);
423 | 		}
424 | 
425 | 		const dependencies =
426 | 			task.dependencies
427 | 				?.map((depId) =>
428 | 					this._buildDependencyGraph(depId, visited, depthMap, depth + 1)
429 | 				)
430 | 				.filter(Boolean) || [];
431 | 
432 | 		return { ...task, dependencies };
433 | 	}
434 | 
435 | 	_formatDependencyChain(node, prefix = '', isLast = true, depth = 0) {
436 | 		if (depth > 3) return '';
437 | 		const connector = isLast ? '└── ' : '├── ';
438 | 		let result = `${prefix}${connector}Task ${node.id}: ${node.title}`;
439 | 		if (node.dependencies?.length) {
440 | 			const childPrefix = prefix + (isLast ? '    ' : '│   ');
441 | 			result += node.dependencies
442 | 				.map((dep, index) =>
443 | 					this._formatDependencyChain(
444 | 						dep,
445 | 						childPrefix,
446 | 						index === node.dependencies.length - 1,
447 | 						depth + 1
448 | 					)
449 | 				)
450 | 				.join('');
451 | 		}
452 | 		return '\n' + result;
453 | 	}
454 | 
455 | 	/**
456 | 	 * Parse task ID strings into structured format
457 | 	 * Supports formats: "15", "15.2", "16,17.1"
458 | 	 * @param {Array<string>} taskIds - Array of task ID strings
459 | 	 * @returns {Array<Object>} Parsed task identifiers
460 | 	 */
461 | 	_parseTaskIds(taskIds) {
462 | 		const parsed = [];
463 | 
464 | 		for (const idStr of taskIds) {
465 | 			if (idStr.includes('.')) {
466 | 				// Subtask format: "15.2"
467 | 				const [parentId, subtaskId] = idStr.split('.');
468 | 				parsed.push({
469 | 					type: 'subtask',
470 | 					parentId: parseInt(parentId, 10),
471 | 					subtaskId: parseInt(subtaskId, 10),
472 | 					fullId: idStr
473 | 				});
474 | 			} else {
475 | 				// Task format: "15"
476 | 				parsed.push({
477 | 					type: 'task',
478 | 					taskId: parseInt(idStr, 10),
479 | 					fullId: idStr
480 | 				});
481 | 			}
482 | 		}
483 | 
484 | 		return parsed;
485 | 	}
486 | 
487 | 	/**
488 | 	 * Gather context from tasks and subtasks
489 | 	 * @param {Array<string>} taskIds - Task/subtask IDs
490 | 	 * @param {string} format - Output format
491 | 	 * @param {boolean} includeTokenCounts - Whether to include token breakdown
492 | 	 * @returns {Promise<Object>} Task context result with breakdown
493 | 	 */
494 | 	async _gatherTaskContext(taskIds, format, includeTokenCounts = false) {
495 | 		try {
496 | 			if (!this.allTasks || this.allTasks.length === 0) {
497 | 				return { context: null, breakdown: [] };
498 | 			}
499 | 
500 | 			const parsedIds = this._parseTaskIds(taskIds);
501 | 			const contextItems = [];
502 | 			const breakdown = [];
503 | 
504 | 			for (const parsed of parsedIds) {
505 | 				let formattedItem = null;
506 | 				let itemInfo = null;
507 | 
508 | 				if (parsed.type === 'task') {
509 | 					const result = findTaskById(this.allTasks, parsed.taskId);
510 | 					if (result.task) {
511 | 						formattedItem = this._formatTaskForContext(result.task, format);
512 | 						itemInfo = {
513 | 							id: parsed.fullId,
514 | 							type: 'task',
515 | 							title: result.task.title,
516 | 							tokens: includeTokenCounts ? this.countTokens(formattedItem) : 0,
517 | 							characters: formattedItem.length
518 | 						};
519 | 					}
520 | 				} else if (parsed.type === 'subtask') {
521 | 					const parentResult = findTaskById(this.allTasks, parsed.parentId);
522 | 					if (parentResult.task && parentResult.task.subtasks) {
523 | 						const subtask = parentResult.task.subtasks.find(
524 | 							(st) => st.id === parsed.subtaskId
525 | 						);
526 | 						if (subtask) {
527 | 							formattedItem = this._formatSubtaskForContext(
528 | 								subtask,
529 | 								parentResult.task,
530 | 								format
531 | 							);
532 | 							itemInfo = {
533 | 								id: parsed.fullId,
534 | 								type: 'subtask',
535 | 								title: subtask.title,
536 | 								parentTitle: parentResult.task.title,
537 | 								tokens: includeTokenCounts
538 | 									? this.countTokens(formattedItem)
539 | 									: 0,
540 | 								characters: formattedItem.length
541 | 							};
542 | 						}
543 | 					}
544 | 				}
545 | 
546 | 				if (formattedItem && itemInfo) {
547 | 					contextItems.push(formattedItem);
548 | 					if (includeTokenCounts) {
549 | 						breakdown.push(itemInfo);
550 | 					}
551 | 				}
552 | 			}
553 | 
554 | 			if (contextItems.length === 0) {
555 | 				return { context: null, breakdown: [] };
556 | 			}
557 | 
558 | 			const finalContext = this._formatTaskContextSection(contextItems, format);
559 | 			return {
560 | 				context: finalContext,
561 | 				breakdown: includeTokenCounts ? breakdown : []
562 | 			};
563 | 		} catch (error) {
564 | 			console.warn(`Warning: Could not gather task context: ${error.message}`);
565 | 			return { context: null, breakdown: [] };
566 | 		}
567 | 	}
568 | 
569 | 	/**
570 | 	 * Format a task for context inclusion
571 | 	 * @param {Object} task - Task object
572 | 	 * @param {string} format - Output format
573 | 	 * @returns {string} Formatted task context
574 | 	 */
575 | 	_formatTaskForContext(task, format) {
576 | 		const sections = [];
577 | 
578 | 		sections.push(`**Task ${task.id}: ${task.title}**`);
579 | 		sections.push(`Description: ${task.description}`);
580 | 		sections.push(`Status: ${task.status || 'pending'}`);
581 | 		sections.push(`Priority: ${task.priority || 'medium'}`);
582 | 
583 | 		if (task.dependencies && task.dependencies.length > 0) {
584 | 			sections.push(`Dependencies: ${task.dependencies.join(', ')}`);
585 | 		}
586 | 
587 | 		if (task.details) {
588 | 			const details = truncate(task.details, 500);
589 | 			sections.push(`Implementation Details: ${details}`);
590 | 		}
591 | 
592 | 		if (task.testStrategy) {
593 | 			const testStrategy = truncate(task.testStrategy, 300);
594 | 			sections.push(`Test Strategy: ${testStrategy}`);
595 | 		}
596 | 
597 | 		if (task.subtasks && task.subtasks.length > 0) {
598 | 			sections.push(`Subtasks: ${task.subtasks.length} subtasks defined`);
599 | 		}
600 | 
601 | 		return sections.join('\n');
602 | 	}
603 | 
604 | 	/**
605 | 	 * Format a subtask for context inclusion
606 | 	 * @param {Object} subtask - Subtask object
607 | 	 * @param {Object} parentTask - Parent task object
608 | 	 * @param {string} format - Output format
609 | 	 * @returns {string} Formatted subtask context
610 | 	 */
611 | 	_formatSubtaskForContext(subtask, parentTask, format) {
612 | 		const sections = [];
613 | 
614 | 		sections.push(
615 | 			`**Subtask ${parentTask.id}.${subtask.id}: ${subtask.title}**`
616 | 		);
617 | 		sections.push(`Parent Task: ${parentTask.title}`);
618 | 		sections.push(`Description: ${subtask.description}`);
619 | 		sections.push(`Status: ${subtask.status || 'pending'}`);
620 | 
621 | 		if (subtask.dependencies && subtask.dependencies.length > 0) {
622 | 			sections.push(`Dependencies: ${subtask.dependencies.join(', ')}`);
623 | 		}
624 | 
625 | 		if (subtask.details) {
626 | 			const details = truncate(subtask.details, 500);
627 | 			sections.push(`Implementation Details: ${details}`);
628 | 		}
629 | 
630 | 		return sections.join('\n');
631 | 	}
632 | 
633 | 	/**
634 | 	 * Gather context from files
635 | 	 * @param {Array<string>} filePaths - File paths to read
636 | 	 * @param {string} format - Output format
637 | 	 * @param {boolean} includeTokenCounts - Whether to include token breakdown
638 | 	 * @returns {Promise<Object>} File context result with breakdown
639 | 	 */
640 | 	async _gatherFileContext(filePaths, format, includeTokenCounts = false) {
641 | 		const fileContents = [];
642 | 		const breakdown = [];
643 | 
644 | 		for (const filePath of filePaths) {
645 | 			try {
646 | 				const fullPath = path.isAbsolute(filePath)
647 | 					? filePath
648 | 					: path.join(this.projectRoot, filePath);
649 | 
650 | 				if (!fs.existsSync(fullPath)) {
651 | 					continue;
652 | 				}
653 | 
654 | 				const stats = fs.statSync(fullPath);
655 | 				if (!stats.isFile()) {
656 | 					continue;
657 | 				}
658 | 
659 | 				// Check file size (limit to 50KB for context)
660 | 				if (stats.size > 50 * 1024) {
661 | 					continue;
662 | 				}
663 | 
664 | 				const content = fs.readFileSync(fullPath, 'utf-8');
665 | 				const relativePath = path.relative(this.projectRoot, fullPath);
666 | 
667 | 				const fileData = {
668 | 					path: relativePath,
669 | 					size: stats.size,
670 | 					content: content,
671 | 					lastModified: stats.mtime
672 | 				};
673 | 
674 | 				fileContents.push(fileData);
675 | 
676 | 				// Calculate tokens for this individual file if requested
677 | 				if (includeTokenCounts) {
678 | 					const formattedFile = this._formatSingleFileForContext(
679 | 						fileData,
680 | 						format
681 | 					);
682 | 					breakdown.push({
683 | 						path: relativePath,
684 | 						sizeKB: Math.round(stats.size / 1024),
685 | 						tokens: this.countTokens(formattedFile),
686 | 						characters: formattedFile.length
687 | 					});
688 | 				}
689 | 			} catch (error) {
690 | 				console.warn(
691 | 					`Warning: Could not read file ${filePath}: ${error.message}`
692 | 				);
693 | 			}
694 | 		}
695 | 
696 | 		if (fileContents.length === 0) {
697 | 			return { context: null, breakdown: [] };
698 | 		}
699 | 
700 | 		const finalContext = this._formatFileContextSection(fileContents, format);
701 | 		return {
702 | 			context: finalContext,
703 | 			breakdown: includeTokenCounts ? breakdown : []
704 | 		};
705 | 	}
706 | 
707 | 	/**
708 | 	 * Generate project file tree context
709 | 	 * @param {string} format - Output format
710 | 	 * @param {boolean} includeTokenCounts - Whether to include token breakdown
711 | 	 * @returns {Promise<Object>} Project tree context result with breakdown
712 | 	 */
713 | 	async _gatherProjectTreeContext(format, includeTokenCounts = false) {
714 | 		try {
715 | 			const tree = this._generateFileTree(this.projectRoot, 5); // Max depth 5
716 | 			const finalContext = this._formatProjectTreeSection(tree, format);
717 | 
718 | 			const breakdown = includeTokenCounts
719 | 				? {
720 | 						tokens: this.countTokens(finalContext),
721 | 						characters: finalContext.length,
722 | 						fileCount: tree.fileCount || 0,
723 | 						dirCount: tree.dirCount || 0
724 | 					}
725 | 				: null;
726 | 
727 | 			return {
728 | 				context: finalContext,
729 | 				breakdown: breakdown
730 | 			};
731 | 		} catch (error) {
732 | 			console.warn(
733 | 				`Warning: Could not generate project tree: ${error.message}`
734 | 			);
735 | 			return { context: null, breakdown: null };
736 | 		}
737 | 	}
738 | 
739 | 	/**
740 | 	 * Format a single file for context (used for token counting)
741 | 	 * @param {Object} fileData - File data object
742 | 	 * @param {string} format - Output format
743 | 	 * @returns {string} Formatted file context
744 | 	 */
745 | 	_formatSingleFileForContext(fileData, format) {
746 | 		const header = `**File: ${fileData.path}** (${Math.round(fileData.size / 1024)}KB)`;
747 | 		const content = `\`\`\`\n${fileData.content}\n\`\`\``;
748 | 		return `${header}\n\n${content}`;
749 | 	}
750 | 
751 | 	/**
752 | 	 * Generate file tree structure
753 | 	 * @param {string} dirPath - Directory path
754 | 	 * @param {number} maxDepth - Maximum depth to traverse
755 | 	 * @param {number} currentDepth - Current depth
756 | 	 * @returns {Object} File tree structure
757 | 	 */
758 | 	_generateFileTree(dirPath, maxDepth, currentDepth = 0) {
759 | 		const ignoreDirs = [
760 | 			'.git',
761 | 			'node_modules',
762 | 			'.env',
763 | 			'coverage',
764 | 			'dist',
765 | 			'build'
766 | 		];
767 | 		const ignoreFiles = ['.DS_Store', '.env', '.env.local', '.env.production'];
768 | 
769 | 		if (currentDepth >= maxDepth) {
770 | 			return null;
771 | 		}
772 | 
773 | 		try {
774 | 			const items = fs.readdirSync(dirPath);
775 | 			const tree = {
776 | 				name: path.basename(dirPath),
777 | 				type: 'directory',
778 | 				children: [],
779 | 				fileCount: 0,
780 | 				dirCount: 0
781 | 			};
782 | 
783 | 			for (const item of items) {
784 | 				if (ignoreDirs.includes(item) || ignoreFiles.includes(item)) {
785 | 					continue;
786 | 				}
787 | 
788 | 				const itemPath = path.join(dirPath, item);
789 | 				const stats = fs.statSync(itemPath);
790 | 
791 | 				if (stats.isDirectory()) {
792 | 					tree.dirCount++;
793 | 					if (currentDepth < maxDepth - 1) {
794 | 						const subtree = this._generateFileTree(
795 | 							itemPath,
796 | 							maxDepth,
797 | 							currentDepth + 1
798 | 						);
799 | 						if (subtree) {
800 | 							tree.children.push(subtree);
801 | 						}
802 | 					}
803 | 				} else {
804 | 					tree.fileCount++;
805 | 					tree.children.push({
806 | 						name: item,
807 | 						type: 'file',
808 | 						size: stats.size
809 | 					});
810 | 				}
811 | 			}
812 | 
813 | 			return tree;
814 | 		} catch (error) {
815 | 			return null;
816 | 		}
817 | 	}
818 | 
819 | 	/**
820 | 	 * Format custom context section
821 | 	 * @param {string} customContext - Custom context string
822 | 	 * @param {string} format - Output format
823 | 	 * @returns {string} Formatted custom context
824 | 	 */
825 | 	_formatCustomContext(customContext, format) {
826 | 		switch (format) {
827 | 			case 'research':
828 | 				return `## Additional Context\n\n${customContext}`;
829 | 			case 'chat':
830 | 				return `**Additional Context:**\n${customContext}`;
831 | 			case 'system-prompt':
832 | 				return `Additional context: ${customContext}`;
833 | 			default:
834 | 				return customContext;
835 | 		}
836 | 	}
837 | 
838 | 	/**
839 | 	 * Format task context section
840 | 	 * @param {Array<string>} taskItems - Formatted task items
841 | 	 * @param {string} format - Output format
842 | 	 * @returns {string} Formatted task context section
843 | 	 */
844 | 	_formatTaskContextSection(taskItems, format) {
845 | 		switch (format) {
846 | 			case 'research':
847 | 				return `## Task Context\n\n${taskItems.join('\n\n---\n\n')}`;
848 | 			case 'chat':
849 | 				return `**Task Context:**\n\n${taskItems.join('\n\n')}`;
850 | 			case 'system-prompt':
851 | 				return `Task context: ${taskItems.join(' | ')}`;
852 | 			default:
853 | 				return taskItems.join('\n\n');
854 | 		}
855 | 	}
856 | 
857 | 	/**
858 | 	 * Format file context section
859 | 	 * @param {Array<Object>} fileContents - File content objects
860 | 	 * @param {string} format - Output format
861 | 	 * @returns {string} Formatted file context section
862 | 	 */
863 | 	_formatFileContextSection(fileContents, format) {
864 | 		const fileItems = fileContents.map((file) => {
865 | 			const header = `**File: ${file.path}** (${Math.round(file.size / 1024)}KB)`;
866 | 			const content = `\`\`\`\n${file.content}\n\`\`\``;
867 | 			return `${header}\n\n${content}`;
868 | 		});
869 | 
870 | 		switch (format) {
871 | 			case 'research':
872 | 				return `## File Context\n\n${fileItems.join('\n\n---\n\n')}`;
873 | 			case 'chat':
874 | 				return `**File Context:**\n\n${fileItems.join('\n\n')}`;
875 | 			case 'system-prompt':
876 | 				return `File context: ${fileContents.map((f) => `${f.path} (${f.content.substring(0, 200)}...)`).join(' | ')}`;
877 | 			default:
878 | 				return fileItems.join('\n\n');
879 | 		}
880 | 	}
881 | 
882 | 	/**
883 | 	 * Format project tree section
884 | 	 * @param {Object} tree - File tree structure
885 | 	 * @param {string} format - Output format
886 | 	 * @returns {string} Formatted project tree section
887 | 	 */
888 | 	_formatProjectTreeSection(tree, format) {
889 | 		const treeString = this._renderFileTree(tree);
890 | 
891 | 		switch (format) {
892 | 			case 'research':
893 | 				return `## Project Structure\n\n\`\`\`\n${treeString}\n\`\`\``;
894 | 			case 'chat':
895 | 				return `**Project Structure:**\n\`\`\`\n${treeString}\n\`\`\``;
896 | 			case 'system-prompt':
897 | 				return `Project structure: ${treeString.replace(/\n/g, ' | ')}`;
898 | 			default:
899 | 				return treeString;
900 | 		}
901 | 	}
902 | 
903 | 	/**
904 | 	 * Render file tree as string
905 | 	 * @param {Object} tree - File tree structure
906 | 	 * @param {string} prefix - Current prefix for indentation
907 | 	 * @returns {string} Rendered tree string
908 | 	 */
909 | 	_renderFileTree(tree, prefix = '') {
910 | 		let result = `${prefix}${tree.name}/`;
911 | 
912 | 		if (tree.fileCount > 0 || tree.dirCount > 0) {
913 | 			result += ` (${tree.fileCount} files, ${tree.dirCount} dirs)`;
914 | 		}
915 | 
916 | 		result += '\n';
917 | 
918 | 		if (tree.children) {
919 | 			tree.children.forEach((child, index) => {
920 | 				const isLast = index === tree.children.length - 1;
921 | 				const childPrefix = prefix + (isLast ? '└── ' : '├── ');
922 | 				const nextPrefix = prefix + (isLast ? '    ' : '│   ');
923 | 
924 | 				if (child.type === 'directory') {
925 | 					result += this._renderFileTree(child, childPrefix);
926 | 				} else {
927 | 					result += `${childPrefix}${child.name}\n`;
928 | 				}
929 | 			});
930 | 		}
931 | 
932 | 		return result;
933 | 	}
934 | 
935 | 	/**
936 | 	 * Join context sections based on format
937 | 	 * @param {Array<string>} sections - Context sections
938 | 	 * @param {string} format - Output format
939 | 	 * @returns {string} Joined context string
940 | 	 */
941 | 	_joinContextSections(sections, format) {
942 | 		if (sections.length === 0) {
943 | 			return '';
944 | 		}
945 | 
946 | 		switch (format) {
947 | 			case 'research':
948 | 				return sections.join('\n\n---\n\n');
949 | 			case 'chat':
950 | 				return sections.join('\n\n');
951 | 			case 'system-prompt':
952 | 				return sections.join(' ');
953 | 			default:
954 | 				return sections.join('\n\n');
955 | 		}
956 | 	}
957 | }
958 | 
959 | /**
960 |  * Factory function to create a context gatherer instance
961 |  * @param {string} projectRoot - Project root directory
962 |  * @param {string} tag - Tag for the task
963 |  * @returns {ContextGatherer} Context gatherer instance
964 |  * @throws {Error} If tag is not provided
965 |  */
966 | export function createContextGatherer(projectRoot, tag) {
967 | 	if (!tag) {
968 | 		throw new Error('Tag is required');
969 | 	}
970 | 	return new ContextGatherer(projectRoot, tag);
971 | }
972 | 
973 | export default ContextGatherer;
974 | 
```
Page 37/52FirstPrevNextLast