#
tokens: 43693/50000 5/821 files (page 33/52)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 33 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

--------------------------------------------------------------------------------
/.taskmaster/docs/task-template-importing-prd.txt:
--------------------------------------------------------------------------------

```
  1 | # Task Template Importing System - Product Requirements Document
  2 | 
  3 | <context>
  4 | # Overview  
  5 | The Task Template Importing system enables seamless integration of external task templates into the Task Master CLI through automatic file discovery. This system allows users to drop task template files into the tasks directory and immediately access them as new tag contexts without manual import commands or configuration. The solution addresses the need for multi-project task management, team collaboration through shared templates, and clean separation between permanent tasks and temporary project contexts.
  6 | 
  7 | # Core Features  
  8 | ## Silent Task Template Discovery
  9 | - **What it does**: Automatically scans for `tasks_*.json` files in the tasks directory during tag operations
 10 | - **Why it's important**: Eliminates friction in adding new task contexts and enables zero-configuration workflow
 11 | - **How it works**: File pattern matching extracts tag names from filenames and validates against internal tag keys
 12 | 
 13 | ## External Tag Resolution System
 14 | - **What it does**: Provides fallback mechanism to external files when tags are not found in main tasks.json
 15 | - **Why it's important**: Maintains clean separation between core tasks and project-specific templates
 16 | - **How it works**: Tag resolution logic checks external files as secondary source while preserving main file precedence
 17 | 
 18 | ## Read-Only External Tag Access
 19 | - **What it does**: Allows viewing and switching to external tags while preventing modifications
 20 | - **Why it's important**: Protects template integrity and prevents accidental changes to shared templates
 21 | - **How it works**: All task modifications route to main tasks.json regardless of current tag context
 22 | 
 23 | ## Tag Precedence Management
 24 | - **What it does**: Ensures main tasks.json tags override external files with same tag names
 25 | - **Why it's important**: Prevents conflicts and maintains data integrity
 26 | - **How it works**: Priority system where main file tags take precedence over external file tags
 27 | 
 28 | # User Experience  
 29 | ## User Personas
 30 | - **Solo Developer**: Manages multiple projects with different task contexts
 31 | - **Team Lead**: Shares standardized task templates across team members
 32 | - **Project Manager**: Organizes tasks by project phases or feature branches
 33 | 
 34 | ## Key User Flows
 35 | ### Template Addition Flow
 36 | 1. User receives or creates a `tasks_projectname.json` file
 37 | 2. User drops file into `.taskmaster/tasks/` directory
 38 | 3. Tag becomes immediately available via `task-master use-tag projectname`
 39 | 4. User can list, view, and switch to external tag without configuration
 40 | 
 41 | ### Template Usage Flow
 42 | 1. User runs `task-master tags` to see available tags including external ones
 43 | 2. External tags display with `(imported)` indicator
 44 | 3. User switches to external tag with `task-master use-tag projectname`
 45 | 4. User can view tasks but modifications are routed to main tasks.json
 46 | 
 47 | ## UI/UX Considerations
 48 | - External tags clearly marked with `(imported)` suffix in listings
 49 | - Visual indicators distinguish between main and external tags
 50 | - Error messages guide users when external files are malformed
 51 | - Read-only warnings when attempting to modify external tag contexts
 52 | </context>
 53 | 
 54 | <PRD>
 55 | # Technical Architecture  
 56 | ## System Components
 57 | 1. **External File Discovery Engine**
 58 |    - File pattern scanner for `tasks_*.json` files
 59 |    - Tag name extraction from filenames using regex
 60 |    - Dynamic tag registry combining main and external sources
 61 |    - Error handling for malformed external files
 62 | 
 63 | 2. **Enhanced Tag Resolution System**
 64 |    - Fallback mechanism to external files when tags not found in main tasks.json
 65 |    - Precedence management ensuring main file tags override external files
 66 |    - Read-only access enforcement for external tags
 67 |    - Tag metadata preservation during discovery operations
 68 | 
 69 | 3. **Silent Discovery Integration**
 70 |    - Automatic scanning during tag-related operations
 71 |    - Seamless integration with existing tag management functions
 72 |    - Zero-configuration workflow requiring no manual import commands
 73 |    - Dynamic tag availability without restart requirements
 74 | 
 75 | ## Data Models
 76 | 
 77 | ### External Task File Structure
 78 | ```json
 79 | {
 80 |   "meta": {
 81 |     "projectName": "External Project Name",
 82 |     "version": "1.0.0",
 83 |     "templateSource": "external",
 84 |     "createdAt": "ISO-8601 timestamp"
 85 |   },
 86 |   "tags": {
 87 |     "projectname": {
 88 |       "meta": {
 89 |         "name": "Project Name",
 90 |         "description": "Project description",
 91 |         "createdAt": "ISO-8601 timestamp"
 92 |       },
 93 |       "tasks": [
 94 |         // Array of task objects
 95 |       ]
 96 |     },
 97 |     "master": {
 98 |       // This section is ignored to prevent conflicts
 99 |     }
100 |   }
101 | }
102 | ```
103 | 
104 | ### Enhanced Tag Registry Model
105 | ```json
106 | {
107 |   "mainTags": [
108 |     {
109 |       "name": "master",
110 |       "source": "main",
111 |       "taskCount": 150,
112 |       "isActive": true
113 |     }
114 |   ],
115 |   "externalTags": [
116 |     {
117 |       "name": "projectname",
118 |       "source": "external",
119 |       "filename": "tasks_projectname.json",
120 |       "taskCount": 25,
121 |       "isReadOnly": true
122 |     }
123 |   ]
124 | }
125 | ```
126 | 
127 | ## APIs and Integrations
128 | 1. **File System Discovery API**
129 |    - Directory scanning with pattern matching
130 |    - JSON file validation and parsing
131 |    - Error handling for corrupted or malformed files
132 |    - File modification time tracking for cache invalidation
133 | 
134 | 2. **Enhanced Tag Management API**
135 |    - `scanForExternalTaskFiles(projectRoot)` - Discover external template files
136 |    - `getExternalTagsFromFiles(projectRoot)` - Extract tag names from external files
137 |    - `readExternalTagData(projectRoot, tagName)` - Read specific external tag data
138 |    - `getAvailableTags(projectRoot)` - Combined main and external tag listing
139 | 
140 | 3. **Tag Resolution Enhancement**
141 |    - Modified `readJSON()` with external file fallback
142 |    - Enhanced `tags()` function with external tag display
143 |    - Updated `useTag()` function supporting external tag switching
144 |    - Read-only enforcement for external tag operations
145 | 
146 | ## Infrastructure Requirements
147 | 1. **File System Access**
148 |    - Read permissions for tasks directory
149 |    - JSON parsing capabilities
150 |    - Pattern matching and regex support
151 |    - Error handling for file system operations
152 | 
153 | 2. **Backward Compatibility**
154 |    - Existing tag operations continue unchanged
155 |    - Main tasks.json structure preserved
156 |    - No breaking changes to current workflows
157 |    - Graceful degradation when external files unavailable
158 | 
159 | # Development Roadmap  
160 | ## Phase 1: Core External File Discovery (Foundation)
161 | 1. **External File Scanner Implementation**
162 |    - Create `scanForExternalTaskFiles()` function in utils.js
163 |    - Implement file pattern matching for `tasks_*.json` files
164 |    - Add error handling for file system access issues
165 |    - Test with various filename patterns and edge cases
166 | 
167 | 2. **Tag Name Extraction System**
168 |    - Implement `getExternalTagsFromFiles()` function
169 |    - Create regex pattern for extracting tag names from filenames
170 |    - Add validation to ensure tag names match internal tag key format
171 |    - Handle special characters and invalid filename patterns
172 | 
173 | 3. **External Tag Data Reader**
174 |    - Create `readExternalTagData()` function
175 |    - Implement JSON parsing with error handling
176 |    - Add validation for required tag structure
177 |    - Ignore 'master' key in external files to prevent conflicts
178 | 
179 | ## Phase 2: Tag Resolution Enhancement (Core Integration)
180 | 1. **Enhanced Tag Registry**
181 |    - Implement `getAvailableTags()` function combining main and external sources
182 |    - Create tag metadata structure including source information
183 |    - Add deduplication logic prioritizing main tags over external
184 |    - Implement caching mechanism for performance optimization
185 | 
186 | 2. **Modified readJSON Function**
187 |    - Add external file fallback when tag not found in main tasks.json
188 |    - Maintain precedence rule: main tasks.json overrides external files
189 |    - Preserve existing error handling and validation patterns
190 |    - Ensure read-only access for external tags
191 | 
192 | 3. **Tag Listing Enhancement**
193 |    - Update `tags()` function to display external tags with `(imported)` indicator
194 |    - Show external tag metadata and task counts
195 |    - Maintain current tag highlighting and sorting functionality
196 |    - Add visual distinction between main and external tags
197 | 
198 | ## Phase 3: User Interface Integration (User Experience)
199 | 1. **Tag Switching Enhancement**
200 |    - Update `useTag()` function to support external tag switching
201 |    - Add read-only warnings when switching to external tags
202 |    - Update state.json with external tag context information
203 |    - Maintain current tag switching behavior for main tags
204 | 
205 | 2. **Error Handling and User Feedback**
206 |    - Implement comprehensive error messages for malformed external files
207 |    - Add user guidance for proper external file structure
208 |    - Create warnings for read-only operations on external tags
209 |    - Ensure graceful degradation when external files are corrupted
210 | 
211 | 3. **Documentation and Help Integration**
212 |    - Update command help text to include external tag information
213 |    - Add examples of external file structure and usage
214 |    - Create troubleshooting guide for common external file issues
215 |    - Document file naming conventions and best practices
216 | 
217 | ## Phase 4: Advanced Features and Optimization (Enhancement)
218 | 1. **Performance Optimization**
219 |    - Implement file modification time caching
220 |    - Add lazy loading for external tag data
221 |    - Optimize file scanning for directories with many files
222 |    - Create efficient tag resolution caching mechanism
223 | 
224 | 2. **Advanced External File Features**
225 |    - Support for nested external file directories
226 |    - Batch external file validation and reporting
227 |    - External file metadata display and management
228 |    - Integration with version control ignore patterns
229 | 
230 | 3. **Team Collaboration Features**
231 |    - Shared external file validation
232 |    - External file conflict detection and resolution
233 |    - Team template sharing guidelines and documentation
234 |    - Integration with git workflows for template management
235 | 
236 | # Logical Dependency Chain
237 | ## Foundation Layer (Must Be Built First)
238 | 1. **External File Scanner** 
239 |    - Core requirement for all other functionality
240 |    - Provides the discovery mechanism for external template files
241 |    - Must handle file system access and pattern matching reliably
242 | 
243 | 2. **Tag Name Extraction**
244 |    - Depends on file scanner functionality
245 |    - Required for identifying available external tags
246 |    - Must validate tag names against internal format requirements
247 | 
248 | 3. **External Tag Data Reader**
249 |    - Depends on tag name extraction
250 |    - Provides access to external tag content
251 |    - Must handle JSON parsing and validation safely
252 | 
253 | ## Integration Layer (Builds on Foundation)
254 | 4. **Enhanced Tag Registry**
255 |    - Depends on all foundation components
256 |    - Combines main and external tag sources
257 |    - Required for unified tag management across the system
258 | 
259 | 5. **Modified readJSON Function**
260 |    - Depends on enhanced tag registry
261 |    - Provides fallback mechanism for tag resolution
262 |    - Critical for maintaining backward compatibility
263 | 
264 | 6. **Tag Listing Enhancement**
265 |    - Depends on enhanced tag registry
266 |    - Provides user visibility into external tags
267 |    - Required for user discovery of available templates
268 | 
269 | ## User Experience Layer (Completes the Feature)
270 | 7. **Tag Switching Enhancement**
271 |    - Depends on modified readJSON and tag listing
272 |    - Enables user interaction with external tags
273 |    - Must enforce read-only access properly
274 | 
275 | 8. **Error Handling and User Feedback**
276 |    - Can be developed in parallel with other UX components
277 |    - Enhances reliability and user experience
278 |    - Should be integrated throughout development process
279 | 
280 | 9. **Documentation and Help Integration**
281 |    - Should be developed alongside implementation
282 |    - Required for user adoption and proper usage
283 |    - Can be completed in parallel with advanced features
284 | 
285 | ## Optimization Layer (Performance and Advanced Features)
286 | 10. **Performance Optimization**
287 |     - Can be developed after core functionality is stable
288 |     - Improves user experience with large numbers of external files
289 |     - Not blocking for initial release
290 | 
291 | 11. **Advanced External File Features**
292 |     - Can be developed independently after core features
293 |     - Enhances power user workflows
294 |     - Optional for initial release
295 | 
296 | 12. **Team Collaboration Features**
297 |     - Depends on stable core functionality
298 |     - Enhances team workflows and template sharing
299 |     - Can be prioritized based on user feedback
300 | 
301 | # Risks and Mitigations  
302 | ## Technical Challenges
303 | 
304 | ### File System Performance
305 | **Risk**: Scanning for external files on every tag operation could impact performance with large directories.
306 | **Mitigation**: 
307 | - Implement file modification time caching to avoid unnecessary rescans
308 | - Use lazy loading for external tag data - only read when accessed
309 | - Add configurable limits on number of external files to scan
310 | - Optimize file pattern matching with efficient regex patterns
311 | 
312 | ### External File Corruption
313 | **Risk**: Malformed or corrupted external JSON files could break tag operations.
314 | **Mitigation**:
315 | - Implement robust JSON parsing with comprehensive error handling
316 | - Add file validation before attempting to parse external files
317 | - Gracefully skip corrupted files and continue with valid ones
318 | - Provide clear error messages guiding users to fix malformed files
319 | 
320 | ### Tag Name Conflicts
321 | **Risk**: External files might contain tag names that conflict with main tasks.json tags.
322 | **Mitigation**:
323 | - Implement strict precedence rule: main tasks.json always overrides external files
324 | - Add warnings when external tags are ignored due to conflicts
325 | - Document naming conventions to avoid common conflicts
326 | - Provide validation tools to check for potential conflicts
327 | 
328 | ## MVP Definition
329 | 
330 | ### Core Feature Scope
331 | **Risk**: Including too many advanced features could delay the core functionality.
332 | **Mitigation**:
333 | - Define MVP as basic external file discovery + tag switching
334 | - Focus on the silent discovery mechanism as the primary value proposition
335 | - Defer advanced features like nested directories and batch operations
336 | - Ensure each phase delivers complete, usable functionality
337 | 
338 | ### User Experience Complexity
339 | **Risk**: The read-only nature of external tags might confuse users.
340 | **Mitigation**:
341 | - Provide clear visual indicators for external tags in all interfaces
342 | - Add explicit warnings when users attempt to modify external tag contexts
343 | - Document the read-only behavior and its rationale clearly
344 | - Consider future enhancement for external tag modification workflows
345 | 
346 | ### Backward Compatibility
347 | **Risk**: Changes to tag resolution logic might break existing workflows.
348 | **Mitigation**:
349 | - Maintain existing tag operations unchanged for main tasks.json
350 | - Add external file support as enhancement, not replacement
351 | - Test thoroughly with existing task structures and workflows
352 | - Provide migration path if any breaking changes are necessary
353 | 
354 | ## Resource Constraints
355 | 
356 | ### Development Complexity
357 | **Risk**: Integration with existing tag management system could be complex.
358 | **Mitigation**:
359 | - Phase implementation to minimize risk of breaking existing functionality
360 | - Create comprehensive test suite covering both main and external tag scenarios
361 | - Use feature flags to enable/disable external file support during development
362 | - Implement thorough error handling to prevent system failures
363 | 
364 | ### File System Dependencies
365 | **Risk**: Different operating systems might handle file operations differently.
366 | **Mitigation**:
367 | - Use Node.js built-in file system APIs for cross-platform compatibility
368 | - Test on multiple operating systems (Windows, macOS, Linux)
369 | - Handle file path separators and naming conventions properly
370 | - Add fallback mechanisms for file system access issues
371 | 
372 | ### User Adoption
373 | **Risk**: Users might not understand or adopt the external file template system.
374 | **Mitigation**:
375 | - Create clear documentation with practical examples
376 | - Provide sample external template files for common use cases
377 | - Integrate help and guidance directly into the CLI interface
378 | - Gather user feedback early and iterate on the user experience
379 | 
380 | # Appendix  
381 | ## External File Naming Convention
382 | 
383 | ### Filename Pattern
384 | - **Format**: `tasks_[tagname].json`
385 | - **Examples**: `tasks_feature-auth.json`, `tasks_v2-migration.json`, `tasks_project-alpha.json`
386 | - **Validation**: Tag name must match internal tag key format (alphanumeric, hyphens, underscores)
387 | 
388 | ### File Structure Requirements
389 | ```json
390 | {
391 |   "meta": {
392 |     "projectName": "Required: Human-readable project name",
393 |     "version": "Optional: Template version",
394 |     "templateSource": "Optional: Source identifier",
395 |     "createdAt": "Optional: ISO-8601 timestamp"
396 |   },
397 |   "tags": {
398 |     "[tagname]": {
399 |       "meta": {
400 |         "name": "Required: Tag display name",
401 |         "description": "Optional: Tag description",
402 |         "createdAt": "Optional: ISO-8601 timestamp"
403 |       },
404 |       "tasks": [
405 |         // Required: Array of task objects following standard task structure
406 |       ]
407 |     }
408 |   }
409 | }
410 | ```
411 | 
412 | ## Implementation Functions Specification
413 | 
414 | ### Core Discovery Functions
415 | ```javascript
416 | // Scan tasks directory for external template files
417 | function scanForExternalTaskFiles(projectRoot) {
418 |   // Returns: Array of external file paths
419 | }
420 | 
421 | // Extract tag names from external filenames
422 | function getExternalTagsFromFiles(projectRoot) {
423 |   // Returns: Array of external tag names
424 | }
425 | 
426 | // Read specific external tag data
427 | function readExternalTagData(projectRoot, tagName) {
428 |   // Returns: Tag data object or null if not found
429 | }
430 | 
431 | // Get combined main and external tags
432 | function getAvailableTags(projectRoot) {
433 |   // Returns: Combined tag registry with metadata
434 | }
435 | ```
436 | 
437 | ### Integration Points
438 | ```javascript
439 | // Enhanced readJSON with external fallback
440 | function readJSON(projectRoot, tag = null) {
441 |   // Modified to check external files when tag not found in main
442 | }
443 | 
444 | // Enhanced tags listing with external indicators
445 | function tags(projectRoot, options = {}) {
446 |   // Modified to display external tags with (imported) suffix
447 | }
448 | 
449 | // Enhanced tag switching with external support
450 | function useTag(projectRoot, tagName) {
451 |   // Modified to support switching to external tags (read-only)
452 | }
453 | ```
454 | 
455 | ## Error Handling Specifications
456 | 
457 | ### File System Errors
458 | - **ENOENT**: External file not found - gracefully skip and continue
459 | - **EACCES**: Permission denied - warn user and continue with available files
460 | - **EISDIR**: Directory instead of file - skip and continue scanning
461 | 
462 | ### JSON Parsing Errors
463 | - **SyntaxError**: Malformed JSON - skip file and log warning with filename
464 | - **Missing required fields**: Skip file and provide specific error message
465 | - **Invalid tag structure**: Skip file and guide user to correct format
466 | 
467 | ### Tag Conflict Resolution
468 | - **Duplicate tag names**: Main tasks.json takes precedence, log warning
469 | - **Invalid tag names**: Skip external file and provide naming guidance
470 | - **Master key in external**: Ignore master key, process other tags normally
471 | </PRD> 
```

--------------------------------------------------------------------------------
/packages/tm-core/src/storage/api-storage.ts:
--------------------------------------------------------------------------------

```typescript
  1 | /**
  2 |  * @fileoverview API-based storage implementation using repository pattern
  3 |  * This provides storage via repository abstraction for flexibility
  4 |  */
  5 | 
  6 | import type {
  7 | 	IStorage,
  8 | 	StorageStats,
  9 | 	UpdateStatusResult
 10 | } from '../interfaces/storage.interface.js';
 11 | import type {
 12 | 	Task,
 13 | 	TaskMetadata,
 14 | 	TaskTag,
 15 | 	TaskStatus
 16 | } from '../types/index.js';
 17 | import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
 18 | import { TaskRepository } from '../repositories/task-repository.interface.js';
 19 | import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
 20 | import { SupabaseClient } from '@supabase/supabase-js';
 21 | import { AuthManager } from '../auth/auth-manager.js';
 22 | 
 23 | /**
 24 |  * API storage configuration
 25 |  */
 26 | export interface ApiStorageConfig {
 27 | 	/** Supabase client instance */
 28 | 	supabaseClient?: SupabaseClient;
 29 | 	/** Custom repository implementation */
 30 | 	repository?: TaskRepository;
 31 | 	/** Project ID for scoping */
 32 | 	projectId: string;
 33 | 	/** Enable request retries */
 34 | 	enableRetry?: boolean;
 35 | 	/** Maximum retry attempts */
 36 | 	maxRetries?: number;
 37 | }
 38 | 
 39 | /**
 40 |  * ApiStorage implementation using repository pattern
 41 |  * Provides flexibility to swap between different backend implementations
 42 |  */
 43 | export class ApiStorage implements IStorage {
 44 | 	private readonly repository: TaskRepository;
 45 | 	private readonly projectId: string;
 46 | 	private readonly enableRetry: boolean;
 47 | 	private readonly maxRetries: number;
 48 | 	private initialized = false;
 49 | 	private tagsCache: Map<string, TaskTag> = new Map();
 50 | 
 51 | 	constructor(config: ApiStorageConfig) {
 52 | 		this.validateConfig(config);
 53 | 
 54 | 		// Use provided repository or create Supabase repository
 55 | 		if (config.repository) {
 56 | 			this.repository = config.repository;
 57 | 		} else if (config.supabaseClient) {
 58 | 			// TODO: SupabaseTaskRepository doesn't implement all TaskRepository methods yet
 59 | 			// Cast for now until full implementation is complete
 60 | 			this.repository = new SupabaseTaskRepository(
 61 | 				config.supabaseClient
 62 | 			) as unknown as TaskRepository;
 63 | 		} else {
 64 | 			throw new TaskMasterError(
 65 | 				'Either repository or supabaseClient must be provided',
 66 | 				ERROR_CODES.MISSING_CONFIGURATION
 67 | 			);
 68 | 		}
 69 | 
 70 | 		this.projectId = config.projectId;
 71 | 		this.enableRetry = config.enableRetry ?? true;
 72 | 		this.maxRetries = config.maxRetries ?? 3;
 73 | 	}
 74 | 
 75 | 	/**
 76 | 	 * Validate API storage configuration
 77 | 	 */
 78 | 	private validateConfig(config: ApiStorageConfig): void {
 79 | 		if (!config.projectId) {
 80 | 			throw new TaskMasterError(
 81 | 				'Project ID is required for API storage',
 82 | 				ERROR_CODES.MISSING_CONFIGURATION
 83 | 			);
 84 | 		}
 85 | 
 86 | 		if (!config.repository && !config.supabaseClient) {
 87 | 			throw new TaskMasterError(
 88 | 				'Either repository or supabaseClient must be provided',
 89 | 				ERROR_CODES.MISSING_CONFIGURATION
 90 | 			);
 91 | 		}
 92 | 	}
 93 | 
 94 | 	/**
 95 | 	 * Initialize the API storage
 96 | 	 */
 97 | 	async initialize(): Promise<void> {
 98 | 		if (this.initialized) return;
 99 | 
100 | 		try {
101 | 			// Load initial tags
102 | 			await this.loadTagsIntoCache();
103 | 			this.initialized = true;
104 | 		} catch (error) {
105 | 			throw new TaskMasterError(
106 | 				'Failed to initialize API storage',
107 | 				ERROR_CODES.STORAGE_ERROR,
108 | 				{ operation: 'initialize' },
109 | 				error as Error
110 | 			);
111 | 		}
112 | 	}
113 | 
114 | 	/**
115 | 	 * Load tags into cache
116 | 	 * In our API-based system, "tags" represent briefs
117 | 	 */
118 | 	private async loadTagsIntoCache(): Promise<void> {
119 | 		try {
120 | 			const authManager = AuthManager.getInstance();
121 | 			const context = authManager.getContext();
122 | 
123 | 			// If we have a selected brief, create a virtual "tag" for it
124 | 			if (context?.briefId) {
125 | 				// Create a virtual tag representing the current brief
126 | 				const briefTag: TaskTag = {
127 | 					name: context.briefId,
128 | 					tasks: [], // Will be populated when tasks are loaded
129 | 					metadata: {
130 | 						briefId: context.briefId,
131 | 						briefName: context.briefName,
132 | 						organizationId: context.orgId
133 | 					}
134 | 				};
135 | 
136 | 				this.tagsCache.clear();
137 | 				this.tagsCache.set(context.briefId, briefTag);
138 | 			}
139 | 		} catch (error) {
140 | 			// If no brief is selected, that's okay - user needs to select one first
141 | 			console.debug('No brief selected, starting with empty cache');
142 | 		}
143 | 	}
144 | 
145 | 	/**
146 | 	 * Load tasks from API
147 | 	 * In our system, the tag parameter represents a brief ID
148 | 	 */
149 | 	async loadTasks(tag?: string): Promise<Task[]> {
150 | 		await this.ensureInitialized();
151 | 
152 | 		try {
153 | 			const authManager = AuthManager.getInstance();
154 | 			const context = authManager.getContext();
155 | 
156 | 			// If no brief is selected in context, throw an error
157 | 			if (!context?.briefId) {
158 | 				throw new Error(
159 | 					'No brief selected. Please select a brief first using: tm context brief <brief-id>'
160 | 				);
161 | 			}
162 | 
163 | 			// Load tasks from the current brief context
164 | 			const tasks = await this.retryOperation(() =>
165 | 				this.repository.getTasks(this.projectId)
166 | 			);
167 | 
168 | 			// Update the tag cache with the loaded task IDs
169 | 			const briefTag = this.tagsCache.get(context.briefId);
170 | 			if (briefTag) {
171 | 				briefTag.tasks = tasks.map((task) => task.id);
172 | 			}
173 | 
174 | 			return tasks;
175 | 		} catch (error) {
176 | 			throw new TaskMasterError(
177 | 				'Failed to load tasks from API',
178 | 				ERROR_CODES.STORAGE_ERROR,
179 | 				{ operation: 'loadTasks', tag, context: 'brief-based loading' },
180 | 				error as Error
181 | 			);
182 | 		}
183 | 	}
184 | 
185 | 	/**
186 | 	 * Save tasks to API
187 | 	 */
188 | 	async saveTasks(tasks: Task[], tag?: string): Promise<void> {
189 | 		await this.ensureInitialized();
190 | 
191 | 		try {
192 | 			if (tag) {
193 | 				// Update tag with task IDs
194 | 				const tagData = this.tagsCache.get(tag) || {
195 | 					name: tag,
196 | 					tasks: [],
197 | 					metadata: {}
198 | 				};
199 | 				tagData.tasks = tasks.map((t) => t.id);
200 | 
201 | 				// Save or update tag
202 | 				if (this.tagsCache.has(tag)) {
203 | 					await this.repository.updateTag(this.projectId, tag, tagData);
204 | 				} else {
205 | 					await this.repository.createTag(this.projectId, tagData);
206 | 				}
207 | 
208 | 				this.tagsCache.set(tag, tagData);
209 | 			}
210 | 
211 | 			// Save tasks using bulk operation
212 | 			await this.retryOperation(() =>
213 | 				this.repository.bulkCreateTasks(this.projectId, tasks)
214 | 			);
215 | 		} catch (error) {
216 | 			throw new TaskMasterError(
217 | 				'Failed to save tasks to API',
218 | 				ERROR_CODES.STORAGE_ERROR,
219 | 				{ operation: 'saveTasks', tag, taskCount: tasks.length },
220 | 				error as Error
221 | 			);
222 | 		}
223 | 	}
224 | 
225 | 	/**
226 | 	 * Load a single task by ID
227 | 	 */
228 | 	async loadTask(taskId: string, tag?: string): Promise<Task | null> {
229 | 		await this.ensureInitialized();
230 | 
231 | 		try {
232 | 			return await this.retryOperation(() =>
233 | 				this.repository.getTask(this.projectId, taskId)
234 | 			);
235 | 		} catch (error) {
236 | 			throw new TaskMasterError(
237 | 				'Failed to load task from API',
238 | 				ERROR_CODES.STORAGE_ERROR,
239 | 				{ operation: 'loadTask', taskId, tag },
240 | 				error as Error
241 | 			);
242 | 		}
243 | 	}
244 | 
245 | 	/**
246 | 	 * Save a single task
247 | 	 */
248 | 	async saveTask(task: Task, tag?: string): Promise<void> {
249 | 		await this.ensureInitialized();
250 | 
251 | 		try {
252 | 			// Check if task exists
253 | 			const existing = await this.repository.getTask(this.projectId, task.id);
254 | 
255 | 			if (existing) {
256 | 				await this.retryOperation(() =>
257 | 					this.repository.updateTask(this.projectId, task.id, task)
258 | 				);
259 | 			} else {
260 | 				await this.retryOperation(() =>
261 | 					this.repository.createTask(this.projectId, task)
262 | 				);
263 | 			}
264 | 
265 | 			// Update tag if specified
266 | 			if (tag) {
267 | 				const tagData = this.tagsCache.get(tag);
268 | 				if (tagData && !tagData.tasks.includes(task.id)) {
269 | 					tagData.tasks.push(task.id);
270 | 					await this.repository.updateTag(this.projectId, tag, tagData);
271 | 				}
272 | 			}
273 | 		} catch (error) {
274 | 			throw new TaskMasterError(
275 | 				'Failed to save task to API',
276 | 				ERROR_CODES.STORAGE_ERROR,
277 | 				{ operation: 'saveTask', taskId: task.id, tag },
278 | 				error as Error
279 | 			);
280 | 		}
281 | 	}
282 | 
283 | 	/**
284 | 	 * Delete a task
285 | 	 */
286 | 	async deleteTask(taskId: string, tag?: string): Promise<void> {
287 | 		await this.ensureInitialized();
288 | 
289 | 		try {
290 | 			await this.retryOperation(() =>
291 | 				this.repository.deleteTask(this.projectId, taskId)
292 | 			);
293 | 
294 | 			// Remove from tag if specified
295 | 			if (tag) {
296 | 				const tagData = this.tagsCache.get(tag);
297 | 				if (tagData) {
298 | 					tagData.tasks = tagData.tasks.filter((id) => id !== taskId);
299 | 					await this.repository.updateTag(this.projectId, tag, tagData);
300 | 				}
301 | 			}
302 | 		} catch (error) {
303 | 			throw new TaskMasterError(
304 | 				'Failed to delete task from API',
305 | 				ERROR_CODES.STORAGE_ERROR,
306 | 				{ operation: 'deleteTask', taskId, tag },
307 | 				error as Error
308 | 			);
309 | 		}
310 | 	}
311 | 
312 | 	/**
313 | 	 * List available tags (briefs in our system)
314 | 	 */
315 | 	async listTags(): Promise<string[]> {
316 | 		await this.ensureInitialized();
317 | 
318 | 		try {
319 | 			const authManager = AuthManager.getInstance();
320 | 			const context = authManager.getContext();
321 | 
322 | 			// In our API-based system, we only have one "tag" at a time - the current brief
323 | 			if (context?.briefId) {
324 | 				// Ensure the current brief is in our cache
325 | 				await this.loadTagsIntoCache();
326 | 				return [context.briefId];
327 | 			}
328 | 
329 | 			// No brief selected, return empty array
330 | 			return [];
331 | 		} catch (error) {
332 | 			throw new TaskMasterError(
333 | 				'Failed to list tags from API',
334 | 				ERROR_CODES.STORAGE_ERROR,
335 | 				{ operation: 'listTags' },
336 | 				error as Error
337 | 			);
338 | 		}
339 | 	}
340 | 
341 | 	/**
342 | 	 * Load metadata
343 | 	 */
344 | 	async loadMetadata(tag?: string): Promise<TaskMetadata | null> {
345 | 		await this.ensureInitialized();
346 | 
347 | 		try {
348 | 			if (tag) {
349 | 				const tagData = this.tagsCache.get(tag);
350 | 				return (tagData?.metadata as TaskMetadata) || null;
351 | 			}
352 | 
353 | 			// Return global metadata if no tag specified
354 | 			// This could be stored in a special system tag
355 | 			const systemTag = await this.repository.getTag(this.projectId, '_system');
356 | 			return (systemTag?.metadata as TaskMetadata) || null;
357 | 		} catch (error) {
358 | 			throw new TaskMasterError(
359 | 				'Failed to load metadata from API',
360 | 				ERROR_CODES.STORAGE_ERROR,
361 | 				{ operation: 'loadMetadata', tag },
362 | 				error as Error
363 | 			);
364 | 		}
365 | 	}
366 | 
367 | 	/**
368 | 	 * Save metadata
369 | 	 */
370 | 	async saveMetadata(metadata: TaskMetadata, tag?: string): Promise<void> {
371 | 		await this.ensureInitialized();
372 | 
373 | 		try {
374 | 			if (tag) {
375 | 				const tagData = this.tagsCache.get(tag) || {
376 | 					name: tag,
377 | 					tasks: [],
378 | 					metadata: {}
379 | 				};
380 | 				tagData.metadata = metadata as any;
381 | 
382 | 				if (this.tagsCache.has(tag)) {
383 | 					await this.repository.updateTag(this.projectId, tag, tagData);
384 | 				} else {
385 | 					await this.repository.createTag(this.projectId, tagData);
386 | 				}
387 | 
388 | 				this.tagsCache.set(tag, tagData);
389 | 			} else {
390 | 				// Save to system tag
391 | 				const systemTag: TaskTag = {
392 | 					name: '_system',
393 | 					tasks: [],
394 | 					metadata: metadata as any
395 | 				};
396 | 
397 | 				const existing = await this.repository.getTag(
398 | 					this.projectId,
399 | 					'_system'
400 | 				);
401 | 				if (existing) {
402 | 					await this.repository.updateTag(this.projectId, '_system', systemTag);
403 | 				} else {
404 | 					await this.repository.createTag(this.projectId, systemTag);
405 | 				}
406 | 			}
407 | 		} catch (error) {
408 | 			throw new TaskMasterError(
409 | 				'Failed to save metadata to API',
410 | 				ERROR_CODES.STORAGE_ERROR,
411 | 				{ operation: 'saveMetadata', tag },
412 | 				error as Error
413 | 			);
414 | 		}
415 | 	}
416 | 
417 | 	/**
418 | 	 * Check if storage exists
419 | 	 */
420 | 	async exists(): Promise<boolean> {
421 | 		try {
422 | 			await this.initialize();
423 | 			return true;
424 | 		} catch {
425 | 			return false;
426 | 		}
427 | 	}
428 | 
429 | 	/**
430 | 	 * Append tasks to existing storage
431 | 	 */
432 | 	async appendTasks(tasks: Task[], tag?: string): Promise<void> {
433 | 		await this.ensureInitialized();
434 | 
435 | 		try {
436 | 			// Use bulk create - repository should handle duplicates
437 | 			await this.retryOperation(() =>
438 | 				this.repository.bulkCreateTasks(this.projectId, tasks)
439 | 			);
440 | 
441 | 			// Update tag if specified
442 | 			if (tag) {
443 | 				const tagData = this.tagsCache.get(tag) || {
444 | 					name: tag,
445 | 					tasks: [],
446 | 					metadata: {}
447 | 				};
448 | 
449 | 				const newTaskIds = tasks.map((t) => t.id);
450 | 				tagData.tasks = [...new Set([...tagData.tasks, ...newTaskIds])];
451 | 
452 | 				if (this.tagsCache.has(tag)) {
453 | 					await this.repository.updateTag(this.projectId, tag, tagData);
454 | 				} else {
455 | 					await this.repository.createTag(this.projectId, tagData);
456 | 				}
457 | 
458 | 				this.tagsCache.set(tag, tagData);
459 | 			}
460 | 		} catch (error) {
461 | 			throw new TaskMasterError(
462 | 				'Failed to append tasks to API',
463 | 				ERROR_CODES.STORAGE_ERROR,
464 | 				{ operation: 'appendTasks', tag, taskCount: tasks.length },
465 | 				error as Error
466 | 			);
467 | 		}
468 | 	}
469 | 
470 | 	/**
471 | 	 * Update a specific task
472 | 	 */
473 | 	async updateTask(
474 | 		taskId: string,
475 | 		updates: Partial<Task>,
476 | 		tag?: string
477 | 	): Promise<void> {
478 | 		await this.ensureInitialized();
479 | 
480 | 		try {
481 | 			await this.retryOperation(() =>
482 | 				this.repository.updateTask(this.projectId, taskId, updates)
483 | 			);
484 | 		} catch (error) {
485 | 			throw new TaskMasterError(
486 | 				'Failed to update task via API',
487 | 				ERROR_CODES.STORAGE_ERROR,
488 | 				{ operation: 'updateTask', taskId, tag },
489 | 				error as Error
490 | 			);
491 | 		}
492 | 	}
493 | 
494 | 	/**
495 | 	 * Update task or subtask status by ID - for API storage
496 | 	 */
497 | 	async updateTaskStatus(
498 | 		taskId: string,
499 | 		newStatus: TaskStatus,
500 | 		tag?: string
501 | 	): Promise<UpdateStatusResult> {
502 | 		await this.ensureInitialized();
503 | 
504 | 		try {
505 | 			const existingTask = await this.retryOperation(() =>
506 | 				this.repository.getTask(this.projectId, taskId)
507 | 			);
508 | 
509 | 			if (!existingTask) {
510 | 				throw new Error(`Task ${taskId} not found`);
511 | 			}
512 | 
513 | 			const oldStatus = existingTask.status;
514 | 			if (oldStatus === newStatus) {
515 | 				return {
516 | 					success: true,
517 | 					oldStatus,
518 | 					newStatus,
519 | 					taskId
520 | 				};
521 | 			}
522 | 
523 | 			// Update the task/subtask status
524 | 			await this.retryOperation(() =>
525 | 				this.repository.updateTask(this.projectId, taskId, {
526 | 					status: newStatus,
527 | 					updatedAt: new Date().toISOString()
528 | 				})
529 | 			);
530 | 
531 | 			// Note: Parent status auto-adjustment is handled by the backend API service
532 | 			// which has its own business logic for managing task relationships
533 | 
534 | 			return {
535 | 				success: true,
536 | 				oldStatus,
537 | 				newStatus,
538 | 				taskId
539 | 			};
540 | 		} catch (error) {
541 | 			throw new TaskMasterError(
542 | 				'Failed to update task status via API',
543 | 				ERROR_CODES.STORAGE_ERROR,
544 | 				{ operation: 'updateTaskStatus', taskId, newStatus, tag },
545 | 				error as Error
546 | 			);
547 | 		}
548 | 	}
549 | 
550 | 	/**
551 | 	 * Get all available tags
552 | 	 */
553 | 	async getAllTags(): Promise<string[]> {
554 | 		return this.listTags();
555 | 	}
556 | 
557 | 	/**
558 | 	 * Delete all tasks for a tag
559 | 	 */
560 | 	async deleteTag(tag: string): Promise<void> {
561 | 		await this.ensureInitialized();
562 | 
563 | 		try {
564 | 			await this.retryOperation(() =>
565 | 				this.repository.deleteTag(this.projectId, tag)
566 | 			);
567 | 
568 | 			this.tagsCache.delete(tag);
569 | 		} catch (error) {
570 | 			throw new TaskMasterError(
571 | 				'Failed to delete tag via API',
572 | 				ERROR_CODES.STORAGE_ERROR,
573 | 				{ operation: 'deleteTag', tag },
574 | 				error as Error
575 | 			);
576 | 		}
577 | 	}
578 | 
579 | 	/**
580 | 	 * Rename a tag
581 | 	 */
582 | 	async renameTag(oldTag: string, newTag: string): Promise<void> {
583 | 		await this.ensureInitialized();
584 | 
585 | 		try {
586 | 			const tagData = this.tagsCache.get(oldTag);
587 | 			if (!tagData) {
588 | 				throw new Error(`Tag ${oldTag} not found`);
589 | 			}
590 | 
591 | 			// Create new tag with same data
592 | 			const newTagData = { ...tagData, name: newTag };
593 | 			await this.repository.createTag(this.projectId, newTagData);
594 | 
595 | 			// Delete old tag
596 | 			await this.repository.deleteTag(this.projectId, oldTag);
597 | 
598 | 			// Update cache
599 | 			this.tagsCache.delete(oldTag);
600 | 			this.tagsCache.set(newTag, newTagData);
601 | 		} catch (error) {
602 | 			throw new TaskMasterError(
603 | 				'Failed to rename tag via API',
604 | 				ERROR_CODES.STORAGE_ERROR,
605 | 				{ operation: 'renameTag', oldTag, newTag },
606 | 				error as Error
607 | 			);
608 | 		}
609 | 	}
610 | 
611 | 	/**
612 | 	 * Copy a tag
613 | 	 */
614 | 	async copyTag(sourceTag: string, targetTag: string): Promise<void> {
615 | 		await this.ensureInitialized();
616 | 
617 | 		try {
618 | 			const sourceData = this.tagsCache.get(sourceTag);
619 | 			if (!sourceData) {
620 | 				throw new Error(`Source tag ${sourceTag} not found`);
621 | 			}
622 | 
623 | 			// Create new tag with copied data
624 | 			const targetData = { ...sourceData, name: targetTag };
625 | 			await this.repository.createTag(this.projectId, targetData);
626 | 
627 | 			// Update cache
628 | 			this.tagsCache.set(targetTag, targetData);
629 | 		} catch (error) {
630 | 			throw new TaskMasterError(
631 | 				'Failed to copy tag via API',
632 | 				ERROR_CODES.STORAGE_ERROR,
633 | 				{ operation: 'copyTag', sourceTag, targetTag },
634 | 				error as Error
635 | 			);
636 | 		}
637 | 	}
638 | 
639 | 	/**
640 | 	 * Get storage statistics
641 | 	 */
642 | 	async getStats(): Promise<StorageStats> {
643 | 		await this.ensureInitialized();
644 | 
645 | 		try {
646 | 			const tasks = await this.repository.getTasks(this.projectId);
647 | 			const tags = await this.repository.getTags(this.projectId);
648 | 
649 | 			const tagStats = tags.map((tag) => ({
650 | 				tag: tag.name,
651 | 				taskCount: tag.tasks.length,
652 | 				lastModified: new Date().toISOString() // TODO: Get actual last modified from tag data
653 | 			}));
654 | 
655 | 			return {
656 | 				totalTasks: tasks.length,
657 | 				totalTags: tags.length,
658 | 				storageSize: 0, // Not applicable for API storage
659 | 				lastModified: new Date().toISOString(),
660 | 				tagStats
661 | 			};
662 | 		} catch (error) {
663 | 			throw new TaskMasterError(
664 | 				'Failed to get stats from API',
665 | 				ERROR_CODES.STORAGE_ERROR,
666 | 				{ operation: 'getStats' },
667 | 				error as Error
668 | 			);
669 | 		}
670 | 	}
671 | 
672 | 	/**
673 | 	 * Create backup
674 | 	 */
675 | 	async backup(): Promise<string> {
676 | 		await this.ensureInitialized();
677 | 
678 | 		try {
679 | 			// Export all data
680 | 			await this.repository.getTasks(this.projectId);
681 | 			await this.repository.getTags(this.projectId);
682 | 
683 | 			// TODO: In a real implementation, this would:
684 | 			// 1. Create backup data structure with tasks and tags
685 | 			// 2. Save the backup to a storage service
686 | 			// For now, return a backup identifier
687 | 			return `backup-${this.projectId}-${Date.now()}`;
688 | 		} catch (error) {
689 | 			throw new TaskMasterError(
690 | 				'Failed to create backup via API',
691 | 				ERROR_CODES.STORAGE_ERROR,
692 | 				{ operation: 'backup' },
693 | 				error as Error
694 | 			);
695 | 		}
696 | 	}
697 | 
698 | 	/**
699 | 	 * Restore from backup
700 | 	 */
701 | 	async restore(backupId: string): Promise<void> {
702 | 		await this.ensureInitialized();
703 | 
704 | 		// This would restore from a backup service
705 | 		// Implementation depends on backup strategy
706 | 		throw new TaskMasterError(
707 | 			'Restore not implemented for API storage',
708 | 			ERROR_CODES.NOT_IMPLEMENTED,
709 | 			{ operation: 'restore', backupId }
710 | 		);
711 | 	}
712 | 
713 | 	/**
714 | 	 * Clear all data
715 | 	 */
716 | 	async clear(): Promise<void> {
717 | 		await this.ensureInitialized();
718 | 
719 | 		try {
720 | 			// Delete all tasks
721 | 			const tasks = await this.repository.getTasks(this.projectId);
722 | 			if (tasks.length > 0) {
723 | 				await this.repository.bulkDeleteTasks(
724 | 					this.projectId,
725 | 					tasks.map((t) => t.id)
726 | 				);
727 | 			}
728 | 
729 | 			// Delete all tags
730 | 			const tags = await this.repository.getTags(this.projectId);
731 | 			for (const tag of tags) {
732 | 				await this.repository.deleteTag(this.projectId, tag.name);
733 | 			}
734 | 
735 | 			// Clear cache
736 | 			this.tagsCache.clear();
737 | 		} catch (error) {
738 | 			throw new TaskMasterError(
739 | 				'Failed to clear data via API',
740 | 				ERROR_CODES.STORAGE_ERROR,
741 | 				{ operation: 'clear' },
742 | 				error as Error
743 | 			);
744 | 		}
745 | 	}
746 | 
747 | 	/**
748 | 	 * Close connection
749 | 	 */
750 | 	async close(): Promise<void> {
751 | 		this.initialized = false;
752 | 		this.tagsCache.clear();
753 | 	}
754 | 
755 | 	/**
756 | 	 * Ensure storage is initialized
757 | 	 */
758 | 	private async ensureInitialized(): Promise<void> {
759 | 		if (!this.initialized) {
760 | 			await this.initialize();
761 | 		}
762 | 	}
763 | 
764 | 	/**
765 | 	 * Retry an operation with exponential backoff
766 | 	 */
767 | 	private async retryOperation<T>(
768 | 		operation: () => Promise<T>,
769 | 		attempt: number = 1
770 | 	): Promise<T> {
771 | 		try {
772 | 			return await operation();
773 | 		} catch (error) {
774 | 			if (this.enableRetry && attempt < this.maxRetries) {
775 | 				const delay = Math.pow(2, attempt) * 1000;
776 | 				await new Promise((resolve) => setTimeout(resolve, delay));
777 | 				return this.retryOperation(operation, attempt + 1);
778 | 			}
779 | 			throw error;
780 | 		}
781 | 	}
782 | }
783 | 
```

--------------------------------------------------------------------------------
/src/ai-providers/gemini-cli.js:
--------------------------------------------------------------------------------

```javascript
  1 | /**
  2 |  * src/ai-providers/gemini-cli.js
  3 |  *
  4 |  * Implementation for interacting with Gemini models via Gemini CLI
  5 |  * using the ai-sdk-provider-gemini-cli package.
  6 |  */
  7 | 
  8 | import { generateObject, generateText, streamText } from 'ai';
  9 | import { parse } from 'jsonc-parser';
 10 | import { BaseAIProvider } from './base-provider.js';
 11 | import { log } from '../../scripts/modules/utils.js';
 12 | 
 13 | let createGeminiProvider;
 14 | 
 15 | async function loadGeminiCliModule() {
 16 | 	if (!createGeminiProvider) {
 17 | 		try {
 18 | 			const mod = await import('ai-sdk-provider-gemini-cli');
 19 | 			createGeminiProvider = mod.createGeminiProvider;
 20 | 		} catch (err) {
 21 | 			throw new Error(
 22 | 				"Gemini CLI SDK is not installed. Please install 'ai-sdk-provider-gemini-cli' to use the gemini-cli provider."
 23 | 			);
 24 | 		}
 25 | 	}
 26 | }
 27 | 
 28 | export class GeminiCliProvider extends BaseAIProvider {
 29 | 	constructor() {
 30 | 		super();
 31 | 		this.name = 'Gemini CLI';
 32 | 	}
 33 | 
 34 | 	/**
 35 | 	 * Override validateAuth to handle Gemini CLI authentication options
 36 | 	 * @param {object} params - Parameters to validate
 37 | 	 */
 38 | 	validateAuth(params) {
 39 | 		// Gemini CLI is designed to use pre-configured OAuth authentication
 40 | 		// Users choose gemini-cli specifically to leverage their existing
 41 | 		// gemini auth login credentials, not to use API keys.
 42 | 		// We support API keys for compatibility, but the expected usage
 43 | 		// is through CLI authentication (no API key required).
 44 | 		// No validation needed - the SDK will handle auth internally
 45 | 	}
 46 | 
 47 | 	/**
 48 | 	 * Creates and returns a Gemini CLI client instance.
 49 | 	 * @param {object} params - Parameters for client initialization
 50 | 	 * @param {string} [params.apiKey] - Optional Gemini API key (rarely used with gemini-cli)
 51 | 	 * @param {string} [params.baseURL] - Optional custom API endpoint
 52 | 	 * @returns {Promise<Function>} Gemini CLI client function
 53 | 	 * @throws {Error} If initialization fails
 54 | 	 */
 55 | 	async getClient(params) {
 56 | 		try {
 57 | 			// Load the Gemini CLI module dynamically
 58 | 			await loadGeminiCliModule();
 59 | 			// Primary use case: Use existing gemini CLI authentication
 60 | 			// Secondary use case: Direct API key (for compatibility)
 61 | 			let authOptions = {};
 62 | 
 63 | 			if (params.apiKey && params.apiKey !== 'gemini-cli-no-key-required') {
 64 | 				// API key provided - use it for compatibility
 65 | 				authOptions = {
 66 | 					authType: 'api-key',
 67 | 					apiKey: params.apiKey
 68 | 				};
 69 | 			} else {
 70 | 				// Expected case: Use gemini CLI authentication via OAuth
 71 | 				authOptions = {
 72 | 					authType: 'oauth-personal'
 73 | 				};
 74 | 			}
 75 | 
 76 | 			// Add baseURL if provided (for custom endpoints)
 77 | 			if (params.baseURL) {
 78 | 				authOptions.baseURL = params.baseURL;
 79 | 			}
 80 | 
 81 | 			// Create and return the provider
 82 | 			return createGeminiProvider(authOptions);
 83 | 		} catch (error) {
 84 | 			this.handleError('client initialization', error);
 85 | 		}
 86 | 	}
 87 | 
 88 | 	/**
 89 | 	 * Extracts system messages from the messages array and returns them separately.
 90 | 	 * This is needed because ai-sdk-provider-gemini-cli expects system prompts as a separate parameter.
 91 | 	 * @param {Array} messages - Array of message objects
 92 | 	 * @param {Object} options - Options for system prompt enhancement
 93 | 	 * @param {boolean} options.enforceJsonOutput - Whether to add JSON enforcement to system prompt
 94 | 	 * @returns {Object} - {systemPrompt: string|undefined, messages: Array}
 95 | 	 */
 96 | 	_extractSystemMessage(messages, options = {}) {
 97 | 		if (!messages || !Array.isArray(messages)) {
 98 | 			return { systemPrompt: undefined, messages: messages || [] };
 99 | 		}
100 | 
101 | 		const systemMessages = messages.filter((msg) => msg.role === 'system');
102 | 		const nonSystemMessages = messages.filter((msg) => msg.role !== 'system');
103 | 
104 | 		// Combine multiple system messages if present
105 | 		let systemPrompt =
106 | 			systemMessages.length > 0
107 | 				? systemMessages.map((msg) => msg.content).join('\n\n')
108 | 				: undefined;
109 | 
110 | 		// Add Gemini CLI specific JSON enforcement if requested
111 | 		if (options.enforceJsonOutput) {
112 | 			const jsonEnforcement = this._getJsonEnforcementPrompt();
113 | 			systemPrompt = systemPrompt
114 | 				? `${systemPrompt}\n\n${jsonEnforcement}`
115 | 				: jsonEnforcement;
116 | 		}
117 | 
118 | 		return { systemPrompt, messages: nonSystemMessages };
119 | 	}
120 | 
121 | 	/**
122 | 	 * Gets a Gemini CLI specific system prompt to enforce strict JSON output
123 | 	 * @returns {string} JSON enforcement system prompt
124 | 	 */
125 | 	_getJsonEnforcementPrompt() {
126 | 		return `CRITICAL: You MUST respond with ONLY valid JSON. Do not include any explanatory text, markdown formatting, code block markers, or conversational phrases like "Here is" or "Of course". Your entire response must be parseable JSON that starts with { or [ and ends with } or ]. No exceptions.`;
127 | 	}
128 | 
129 | 	/**
130 | 	 * Checks if a string is valid JSON
131 | 	 * @param {string} text - Text to validate
132 | 	 * @returns {boolean} True if valid JSON
133 | 	 */
134 | 	_isValidJson(text) {
135 | 		if (!text || typeof text !== 'string') {
136 | 			return false;
137 | 		}
138 | 
139 | 		try {
140 | 			JSON.parse(text.trim());
141 | 			return true;
142 | 		} catch {
143 | 			return false;
144 | 		}
145 | 	}
146 | 
147 | 	/**
148 | 	 * Detects if the user prompt is requesting JSON output
149 | 	 * @param {Array} messages - Array of message objects
150 | 	 * @returns {boolean} True if JSON output is likely expected
151 | 	 */
152 | 	_detectJsonRequest(messages) {
153 | 		const userMessages = messages.filter((msg) => msg.role === 'user');
154 | 		const combinedText = userMessages
155 | 			.map((msg) => msg.content)
156 | 			.join(' ')
157 | 			.toLowerCase();
158 | 
159 | 		// Look for indicators that JSON output is expected
160 | 		const jsonIndicators = [
161 | 			'json',
162 | 			'respond only with',
163 | 			'return only',
164 | 			'output only',
165 | 			'format:',
166 | 			'structure:',
167 | 			'schema:',
168 | 			'{"',
169 | 			'[{',
170 | 			'subtasks',
171 | 			'array',
172 | 			'object'
173 | 		];
174 | 
175 | 		return jsonIndicators.some((indicator) => combinedText.includes(indicator));
176 | 	}
177 | 
178 | 	/**
179 | 	 * Simplifies complex prompts for gemini-cli to improve JSON output compliance
180 | 	 * @param {Array} messages - Array of message objects
181 | 	 * @returns {Array} Simplified messages array
182 | 	 */
183 | 	_simplifyJsonPrompts(messages) {
184 | 		// First, check if this is an expand-task operation by looking at the system message
185 | 		const systemMsg = messages.find((m) => m.role === 'system');
186 | 		const isExpandTask =
187 | 			systemMsg &&
188 | 			systemMsg.content.includes(
189 | 				'You are an AI assistant helping with task breakdown. Generate exactly'
190 | 			);
191 | 
192 | 		if (!isExpandTask) {
193 | 			return messages; // Not an expand task, return unchanged
194 | 		}
195 | 
196 | 		// Extract subtask count from system message
197 | 		const subtaskCountMatch = systemMsg.content.match(
198 | 			/Generate exactly (\d+) subtasks/
199 | 		);
200 | 		const subtaskCount = subtaskCountMatch ? subtaskCountMatch[1] : '10';
201 | 
202 | 		log(
203 | 			'debug',
204 | 			`${this.name} detected expand-task operation, simplifying for ${subtaskCount} subtasks`
205 | 		);
206 | 
207 | 		return messages.map((msg) => {
208 | 			if (msg.role !== 'user') {
209 | 				return msg;
210 | 			}
211 | 
212 | 			// For expand-task user messages, create a much simpler, more direct prompt
213 | 			// that doesn't depend on specific task content
214 | 			const simplifiedPrompt = `Generate exactly ${subtaskCount} subtasks in the following JSON format.
215 | 
216 | CRITICAL INSTRUCTION: You must respond with ONLY valid JSON. No explanatory text, no "Here is", no "Of course", no markdown - just the JSON object.
217 | 
218 | Required JSON structure:
219 | {
220 |   "subtasks": [
221 |     {
222 |       "id": 1,
223 |       "title": "Specific actionable task title",
224 |       "description": "Clear task description",
225 |       "dependencies": [],
226 |       "details": "Implementation details and guidance",
227 |       "testStrategy": "Testing approach"
228 |     }
229 |   ]
230 | }
231 | 
232 | Generate ${subtaskCount} subtasks based on the original task context. Return ONLY the JSON object.`;
233 | 
234 | 			log(
235 | 				'debug',
236 | 				`${this.name} simplified user prompt for better JSON compliance`
237 | 			);
238 | 			return { ...msg, content: simplifiedPrompt };
239 | 		});
240 | 	}
241 | 
242 | 	/**
243 | 	 * Extract JSON from Gemini's response using a tolerant parser.
244 | 	 *
245 | 	 * Optimized approach that progressively tries different parsing strategies:
246 | 	 * 1. Direct parsing after cleanup
247 | 	 * 2. Smart boundary detection with single-pass analysis
248 | 	 * 3. Limited character-by-character fallback for edge cases
249 | 	 *
250 | 	 * @param {string} text - Raw text which may contain JSON
251 | 	 * @returns {string} A valid JSON string if extraction succeeds, otherwise the original text
252 | 	 */
253 | 	extractJson(text) {
254 | 		if (!text || typeof text !== 'string') {
255 | 			return text;
256 | 		}
257 | 
258 | 		let content = text.trim();
259 | 
260 | 		// Early exit for very short content
261 | 		if (content.length < 2) {
262 | 			return text;
263 | 		}
264 | 
265 | 		// Strip common wrappers in a single pass
266 | 		content = content
267 | 			// Remove markdown fences
268 | 			.replace(/^.*?```(?:json)?\s*([\s\S]*?)\s*```.*$/i, '$1')
269 | 			// Remove variable declarations
270 | 			.replace(/^\s*(?:const|let|var)\s+\w+\s*=\s*([\s\S]*?)(?:;|\s*)$/i, '$1')
271 | 			// Remove common prefixes
272 | 			.replace(/^(?:Here's|The)\s+(?:the\s+)?JSON.*?[:]\s*/i, '')
273 | 			.trim();
274 | 
275 | 		// Find the first JSON-like structure
276 | 		const firstObj = content.indexOf('{');
277 | 		const firstArr = content.indexOf('[');
278 | 
279 | 		if (firstObj === -1 && firstArr === -1) {
280 | 			return text;
281 | 		}
282 | 
283 | 		const start =
284 | 			firstArr === -1
285 | 				? firstObj
286 | 				: firstObj === -1
287 | 					? firstArr
288 | 					: Math.min(firstObj, firstArr);
289 | 		content = content.slice(start);
290 | 
291 | 		// Optimized parsing function with error collection
292 | 		const tryParse = (value) => {
293 | 			if (!value || value.length < 2) return undefined;
294 | 
295 | 			const errors = [];
296 | 			try {
297 | 				const result = parse(value, errors, {
298 | 					allowTrailingComma: true,
299 | 					allowEmptyContent: false
300 | 				});
301 | 				if (errors.length === 0 && result !== undefined) {
302 | 					return JSON.stringify(result, null, 2);
303 | 				}
304 | 			} catch {
305 | 				// Parsing failed completely
306 | 			}
307 | 			return undefined;
308 | 		};
309 | 
310 | 		// Try parsing the full content first
311 | 		const fullParse = tryParse(content);
312 | 		if (fullParse !== undefined) {
313 | 			return fullParse;
314 | 		}
315 | 
316 | 		// Smart boundary detection - single pass with optimizations
317 | 		const openChar = content[0];
318 | 		const closeChar = openChar === '{' ? '}' : ']';
319 | 
320 | 		let depth = 0;
321 | 		let inString = false;
322 | 		let escapeNext = false;
323 | 		let lastValidEnd = -1;
324 | 
325 | 		// Single-pass boundary detection with early termination
326 | 		for (let i = 0; i < content.length && i < 10000; i++) {
327 | 			// Limit scan for performance
328 | 			const char = content[i];
329 | 
330 | 			if (escapeNext) {
331 | 				escapeNext = false;
332 | 				continue;
333 | 			}
334 | 
335 | 			if (char === '\\') {
336 | 				escapeNext = true;
337 | 				continue;
338 | 			}
339 | 
340 | 			if (char === '"') {
341 | 				inString = !inString;
342 | 				continue;
343 | 			}
344 | 
345 | 			if (inString) continue;
346 | 
347 | 			if (char === openChar) {
348 | 				depth++;
349 | 			} else if (char === closeChar) {
350 | 				depth--;
351 | 				if (depth === 0) {
352 | 					lastValidEnd = i + 1;
353 | 					// Try parsing immediately on first valid boundary
354 | 					const candidate = content.slice(0, lastValidEnd);
355 | 					const parsed = tryParse(candidate);
356 | 					if (parsed !== undefined) {
357 | 						return parsed;
358 | 					}
359 | 				}
360 | 			}
361 | 		}
362 | 
363 | 		// If we found valid boundaries but parsing failed, try limited fallback
364 | 		if (lastValidEnd > 0) {
365 | 			const maxAttempts = Math.min(5, Math.floor(lastValidEnd / 100)); // Limit attempts
366 | 			for (let i = 0; i < maxAttempts; i++) {
367 | 				const testEnd = Math.max(
368 | 					lastValidEnd - i * 50,
369 | 					Math.floor(lastValidEnd * 0.8)
370 | 				);
371 | 				const candidate = content.slice(0, testEnd);
372 | 				const parsed = tryParse(candidate);
373 | 				if (parsed !== undefined) {
374 | 					return parsed;
375 | 				}
376 | 			}
377 | 		}
378 | 
379 | 		return text;
380 | 	}
381 | 
382 | 	/**
383 | 	 * Generates text using Gemini CLI model
384 | 	 * Overrides base implementation to properly handle system messages and enforce JSON output when needed
385 | 	 */
386 | 	async generateText(params) {
387 | 		try {
388 | 			this.validateParams(params);
389 | 			this.validateMessages(params.messages);
390 | 
391 | 			log(
392 | 				'debug',
393 | 				`Generating ${this.name} text with model: ${params.modelId}`
394 | 			);
395 | 
396 | 			// Detect if JSON output is expected and enforce it for better gemini-cli compatibility
397 | 			const enforceJsonOutput = this._detectJsonRequest(params.messages);
398 | 
399 | 			// Debug logging to understand what's happening
400 | 			log('debug', `${this.name} JSON detection analysis:`, {
401 | 				enforceJsonOutput,
402 | 				messageCount: params.messages.length,
403 | 				messages: params.messages.map((msg) => ({
404 | 					role: msg.role,
405 | 					contentPreview: msg.content
406 | 						? msg.content.substring(0, 200) + '...'
407 | 						: 'empty'
408 | 				}))
409 | 			});
410 | 
411 | 			if (enforceJsonOutput) {
412 | 				log(
413 | 					'debug',
414 | 					`${this.name} detected JSON request - applying strict JSON enforcement system prompt`
415 | 				);
416 | 			}
417 | 
418 | 			// For gemini-cli, simplify complex prompts before processing
419 | 			let processedMessages = params.messages;
420 | 			if (enforceJsonOutput) {
421 | 				processedMessages = this._simplifyJsonPrompts(params.messages);
422 | 			}
423 | 
424 | 			// Extract system messages for separate handling with optional JSON enforcement
425 | 			const { systemPrompt, messages } = this._extractSystemMessage(
426 | 				processedMessages,
427 | 				{ enforceJsonOutput }
428 | 			);
429 | 
430 | 			// Debug the final system prompt being sent
431 | 			log('debug', `${this.name} final system prompt:`, {
432 | 				systemPromptLength: systemPrompt ? systemPrompt.length : 0,
433 | 				systemPromptPreview: systemPrompt
434 | 					? systemPrompt.substring(0, 300) + '...'
435 | 					: 'none',
436 | 				finalMessageCount: messages.length
437 | 			});
438 | 
439 | 			const client = await this.getClient(params);
440 | 			const result = await generateText({
441 | 				model: client(params.modelId),
442 | 				system: systemPrompt,
443 | 				messages: messages,
444 | 				maxTokens: params.maxTokens,
445 | 				temperature: params.temperature
446 | 			});
447 | 
448 | 			// If we detected a JSON request and gemini-cli returned conversational text,
449 | 			// attempt to extract JSON from the response
450 | 			let finalText = result.text;
451 | 			if (enforceJsonOutput && result.text && !this._isValidJson(result.text)) {
452 | 				log(
453 | 					'debug',
454 | 					`${this.name} response appears conversational, attempting JSON extraction`
455 | 				);
456 | 
457 | 				// Log first 1000 chars of the response to see what Gemini actually returned
458 | 				log('debug', `${this.name} raw response preview:`, {
459 | 					responseLength: result.text.length,
460 | 					responseStart: result.text.substring(0, 1000)
461 | 				});
462 | 
463 | 				const extractedJson = this.extractJson(result.text);
464 | 				if (this._isValidJson(extractedJson)) {
465 | 					log(
466 | 						'debug',
467 | 						`${this.name} successfully extracted JSON from conversational response`
468 | 					);
469 | 					finalText = extractedJson;
470 | 				} else {
471 | 					log(
472 | 						'debug',
473 | 						`${this.name} JSON extraction failed, returning original response`
474 | 					);
475 | 
476 | 					// Log what extraction returned to debug why it failed
477 | 					log('debug', `${this.name} extraction result preview:`, {
478 | 						extractedLength: extractedJson ? extractedJson.length : 0,
479 | 						extractedStart: extractedJson
480 | 							? extractedJson.substring(0, 500)
481 | 							: 'null'
482 | 					});
483 | 				}
484 | 			}
485 | 
486 | 			log(
487 | 				'debug',
488 | 				`${this.name} generateText completed successfully for model: ${params.modelId}`
489 | 			);
490 | 
491 | 			return {
492 | 				text: finalText,
493 | 				usage: {
494 | 					inputTokens: result.usage?.promptTokens,
495 | 					outputTokens: result.usage?.completionTokens,
496 | 					totalTokens: result.usage?.totalTokens
497 | 				}
498 | 			};
499 | 		} catch (error) {
500 | 			this.handleError('text generation', error);
501 | 		}
502 | 	}
503 | 
504 | 	/**
505 | 	 * Streams text using Gemini CLI model
506 | 	 * Overrides base implementation to properly handle system messages and enforce JSON output when needed
507 | 	 */
508 | 	async streamText(params) {
509 | 		try {
510 | 			this.validateParams(params);
511 | 			this.validateMessages(params.messages);
512 | 
513 | 			log('debug', `Streaming ${this.name} text with model: ${params.modelId}`);
514 | 
515 | 			// Detect if JSON output is expected and enforce it for better gemini-cli compatibility
516 | 			const enforceJsonOutput = this._detectJsonRequest(params.messages);
517 | 
518 | 			// Debug logging to understand what's happening
519 | 			log('debug', `${this.name} JSON detection analysis:`, {
520 | 				enforceJsonOutput,
521 | 				messageCount: params.messages.length,
522 | 				messages: params.messages.map((msg) => ({
523 | 					role: msg.role,
524 | 					contentPreview: msg.content
525 | 						? msg.content.substring(0, 200) + '...'
526 | 						: 'empty'
527 | 				}))
528 | 			});
529 | 
530 | 			if (enforceJsonOutput) {
531 | 				log(
532 | 					'debug',
533 | 					`${this.name} detected JSON request - applying strict JSON enforcement system prompt`
534 | 				);
535 | 			}
536 | 
537 | 			// Extract system messages for separate handling with optional JSON enforcement
538 | 			const { systemPrompt, messages } = this._extractSystemMessage(
539 | 				params.messages,
540 | 				{ enforceJsonOutput }
541 | 			);
542 | 
543 | 			const client = await this.getClient(params);
544 | 			const stream = await streamText({
545 | 				model: client(params.modelId),
546 | 				system: systemPrompt,
547 | 				messages: messages,
548 | 				maxTokens: params.maxTokens,
549 | 				temperature: params.temperature
550 | 			});
551 | 
552 | 			log(
553 | 				'debug',
554 | 				`${this.name} streamText initiated successfully for model: ${params.modelId}`
555 | 			);
556 | 
557 | 			// Note: For streaming, we can't intercept and modify the response in real-time
558 | 			// The JSON extraction would need to happen on the consuming side
559 | 			return stream;
560 | 		} catch (error) {
561 | 			this.handleError('text streaming', error);
562 | 		}
563 | 	}
564 | 
565 | 	/**
566 | 	 * Generates a structured object using Gemini CLI model
567 | 	 * Overrides base implementation to handle Gemini-specific JSON formatting issues and system messages
568 | 	 */
569 | 	async generateObject(params) {
570 | 		try {
571 | 			// First try the standard generateObject from base class
572 | 			return await super.generateObject(params);
573 | 		} catch (error) {
574 | 			// If it's a JSON parsing error, try to extract and parse JSON manually
575 | 			if (error.message?.includes('JSON') || error.message?.includes('parse')) {
576 | 				log(
577 | 					'debug',
578 | 					`Gemini CLI generateObject failed with parsing error, attempting manual extraction`
579 | 				);
580 | 
581 | 				try {
582 | 					// Validate params first
583 | 					this.validateParams(params);
584 | 					this.validateMessages(params.messages);
585 | 
586 | 					if (!params.schema) {
587 | 						throw new Error('Schema is required for object generation');
588 | 					}
589 | 					if (!params.objectName) {
590 | 						throw new Error('Object name is required for object generation');
591 | 					}
592 | 
593 | 					// Extract system messages for separate handling with JSON enforcement
594 | 					const { systemPrompt, messages } = this._extractSystemMessage(
595 | 						params.messages,
596 | 						{ enforceJsonOutput: true }
597 | 					);
598 | 
599 | 					// Call generateObject directly with our client
600 | 					const client = await this.getClient(params);
601 | 					const result = await generateObject({
602 | 						model: client(params.modelId),
603 | 						system: systemPrompt,
604 | 						messages: messages,
605 | 						schema: params.schema,
606 | 						mode: 'json', // Use json mode instead of auto for Gemini
607 | 						maxTokens: params.maxTokens,
608 | 						temperature: params.temperature
609 | 					});
610 | 
611 | 					// If we get rawResponse text, try to extract JSON from it
612 | 					if (result.rawResponse?.text && !result.object) {
613 | 						const extractedJson = this.extractJson(result.rawResponse.text);
614 | 						try {
615 | 							result.object = JSON.parse(extractedJson);
616 | 						} catch (parseError) {
617 | 							log(
618 | 								'error',
619 | 								`Failed to parse extracted JSON: ${parseError.message}`
620 | 							);
621 | 							log(
622 | 								'debug',
623 | 								`Extracted JSON: ${extractedJson.substring(0, 500)}...`
624 | 							);
625 | 							throw new Error(
626 | 								`Gemini CLI returned invalid JSON that could not be parsed: ${parseError.message}`
627 | 							);
628 | 						}
629 | 					}
630 | 
631 | 					return {
632 | 						object: result.object,
633 | 						usage: {
634 | 							inputTokens: result.usage?.promptTokens,
635 | 							outputTokens: result.usage?.completionTokens,
636 | 							totalTokens: result.usage?.totalTokens
637 | 						}
638 | 					};
639 | 				} catch (retryError) {
640 | 					log(
641 | 						'error',
642 | 						`Gemini CLI manual JSON extraction failed: ${retryError.message}`
643 | 					);
644 | 					// Re-throw the original error with more context
645 | 					throw new Error(
646 | 						`${this.name} failed to generate valid JSON object: ${error.message}`
647 | 					);
648 | 				}
649 | 			}
650 | 
651 | 			// For non-parsing errors, just re-throw
652 | 			throw error;
653 | 		}
654 | 	}
655 | 
656 | 	getRequiredApiKeyName() {
657 | 		return 'GEMINI_API_KEY';
658 | 	}
659 | 
660 | 	isRequiredApiKey() {
661 | 		return false;
662 | 	}
663 | }
664 | 
```

--------------------------------------------------------------------------------
/tests/integration/move-task-cross-tag.integration.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | import fs from 'fs';
  3 | import path from 'path';
  4 | import { fileURLToPath } from 'url';
  5 | 
  6 | const __filename = fileURLToPath(import.meta.url);
  7 | const __dirname = path.dirname(__filename);
  8 | 
  9 | // Mock dependencies before importing
 10 | const mockUtils = {
 11 | 	readJSON: jest.fn(),
 12 | 	writeJSON: jest.fn(),
 13 | 	findProjectRoot: jest.fn(() => '/test/project/root'),
 14 | 	log: jest.fn(),
 15 | 	setTasksForTag: jest.fn(),
 16 | 	traverseDependencies: jest.fn((sourceTasks, allTasks, options = {}) => {
 17 | 		// Mock realistic dependency behavior for testing
 18 | 		const { direction = 'forward' } = options;
 19 | 
 20 | 		if (direction === 'forward') {
 21 | 			// Return dependencies that tasks have
 22 | 			const result = [];
 23 | 			sourceTasks.forEach((task) => {
 24 | 				if (task.dependencies && Array.isArray(task.dependencies)) {
 25 | 					result.push(...task.dependencies);
 26 | 				}
 27 | 			});
 28 | 			return result;
 29 | 		} else if (direction === 'reverse') {
 30 | 			// Return tasks that depend on the source tasks
 31 | 			const sourceIds = sourceTasks.map((t) => t.id);
 32 | 			const normalizedSourceIds = sourceIds.map((id) => String(id));
 33 | 			const result = [];
 34 | 			allTasks.forEach((task) => {
 35 | 				if (task.dependencies && Array.isArray(task.dependencies)) {
 36 | 					const hasDependency = task.dependencies.some((depId) =>
 37 | 						normalizedSourceIds.includes(String(depId))
 38 | 					);
 39 | 					if (hasDependency) {
 40 | 						result.push(task.id);
 41 | 					}
 42 | 				}
 43 | 			});
 44 | 			return result;
 45 | 		}
 46 | 		return [];
 47 | 	})
 48 | };
 49 | 
 50 | // Mock the utils module
 51 | jest.unstable_mockModule('../../scripts/modules/utils.js', () => mockUtils);
 52 | 
 53 | // Mock other dependencies
 54 | jest.unstable_mockModule(
 55 | 	'../../scripts/modules/task-manager/is-task-dependent.js',
 56 | 	() => ({
 57 | 		default: jest.fn(() => false)
 58 | 	})
 59 | );
 60 | 
 61 | jest.unstable_mockModule('../../scripts/modules/dependency-manager.js', () => ({
 62 | 	findCrossTagDependencies: jest.fn(() => {
 63 | 		// Since dependencies can only exist within the same tag,
 64 | 		// this function should never find any cross-tag conflicts
 65 | 		return [];
 66 | 	}),
 67 | 	getDependentTaskIds: jest.fn(
 68 | 		(sourceTasks, crossTagDependencies, allTasks) => {
 69 | 			// Since we now use findAllDependenciesRecursively in the actual implementation,
 70 | 			// this mock simulates finding all dependencies recursively within the same tag
 71 | 			const dependentIds = new Set();
 72 | 			const processedIds = new Set();
 73 | 
 74 | 			function findAllDependencies(taskId) {
 75 | 				if (processedIds.has(taskId)) return;
 76 | 				processedIds.add(taskId);
 77 | 
 78 | 				const task = allTasks.find((t) => t.id === taskId);
 79 | 				if (!task || !Array.isArray(task.dependencies)) return;
 80 | 
 81 | 				task.dependencies.forEach((depId) => {
 82 | 					const normalizedDepId =
 83 | 						typeof depId === 'string' ? parseInt(depId, 10) : depId;
 84 | 					if (!isNaN(normalizedDepId) && normalizedDepId !== taskId) {
 85 | 						dependentIds.add(normalizedDepId);
 86 | 						findAllDependencies(normalizedDepId);
 87 | 					}
 88 | 				});
 89 | 			}
 90 | 
 91 | 			sourceTasks.forEach((sourceTask) => {
 92 | 				if (sourceTask && sourceTask.id) {
 93 | 					findAllDependencies(sourceTask.id);
 94 | 				}
 95 | 			});
 96 | 
 97 | 			return Array.from(dependentIds);
 98 | 		}
 99 | 	),
100 | 	validateSubtaskMove: jest.fn((taskId, sourceTag, targetTag) => {
101 | 		// Throw error for subtask IDs
102 | 		const taskIdStr = String(taskId);
103 | 		if (taskIdStr.includes('.')) {
104 | 			throw new Error('Cannot move subtasks directly between tags');
105 | 		}
106 | 	})
107 | }));
108 | 
109 | jest.unstable_mockModule(
110 | 	'../../scripts/modules/task-manager/generate-task-files.js',
111 | 	() => ({
112 | 		default: jest.fn().mockResolvedValue()
113 | 	})
114 | );
115 | 
116 | // Import the modules we'll be testing after mocking
117 | const { moveTasksBetweenTags } = await import(
118 | 	'../../scripts/modules/task-manager/move-task.js'
119 | );
120 | 
121 | describe('Cross-Tag Task Movement Integration Tests', () => {
122 | 	let testDataPath;
123 | 	let mockTasksData;
124 | 
125 | 	beforeEach(() => {
126 | 		// Setup test data path
127 | 		testDataPath = path.join(__dirname, 'temp-test-tasks.json');
128 | 
129 | 		// Initialize mock data with multiple tags
130 | 		mockTasksData = {
131 | 			backlog: {
132 | 				tasks: [
133 | 					{
134 | 						id: 1,
135 | 						title: 'Backlog Task 1',
136 | 						description: 'A task in backlog',
137 | 						status: 'pending',
138 | 						dependencies: [],
139 | 						priority: 'medium',
140 | 						tag: 'backlog'
141 | 					},
142 | 					{
143 | 						id: 2,
144 | 						title: 'Backlog Task 2',
145 | 						description: 'Another task in backlog',
146 | 						status: 'pending',
147 | 						dependencies: [1],
148 | 						priority: 'high',
149 | 						tag: 'backlog'
150 | 					},
151 | 					{
152 | 						id: 3,
153 | 						title: 'Backlog Task 3',
154 | 						description: 'Independent task',
155 | 						status: 'pending',
156 | 						dependencies: [],
157 | 						priority: 'low',
158 | 						tag: 'backlog'
159 | 					}
160 | 				]
161 | 			},
162 | 			'in-progress': {
163 | 				tasks: [
164 | 					{
165 | 						id: 4,
166 | 						title: 'In Progress Task 1',
167 | 						description: 'A task being worked on',
168 | 						status: 'in-progress',
169 | 						dependencies: [],
170 | 						priority: 'high',
171 | 						tag: 'in-progress'
172 | 					}
173 | 				]
174 | 			},
175 | 			done: {
176 | 				tasks: [
177 | 					{
178 | 						id: 5,
179 | 						title: 'Completed Task 1',
180 | 						description: 'A completed task',
181 | 						status: 'done',
182 | 						dependencies: [],
183 | 						priority: 'medium',
184 | 						tag: 'done'
185 | 					}
186 | 				]
187 | 			}
188 | 		};
189 | 
190 | 		// Setup mock utils
191 | 		mockUtils.readJSON.mockReturnValue(mockTasksData);
192 | 		mockUtils.writeJSON.mockImplementation((path, data, projectRoot, tag) => {
193 | 			// Simulate writing to file
194 | 			return Promise.resolve();
195 | 		});
196 | 	});
197 | 
198 | 	afterEach(() => {
199 | 		jest.clearAllMocks();
200 | 		// Clean up temp file if it exists
201 | 		if (fs.existsSync(testDataPath)) {
202 | 			fs.unlinkSync(testDataPath);
203 | 		}
204 | 	});
205 | 
206 | 	describe('Basic Cross-Tag Movement', () => {
207 | 		it('should move a single task between tags successfully', async () => {
208 | 			const taskIds = [1];
209 | 			const sourceTag = 'backlog';
210 | 			const targetTag = 'in-progress';
211 | 
212 | 			const result = await moveTasksBetweenTags(
213 | 				testDataPath,
214 | 				taskIds,
215 | 				sourceTag,
216 | 				targetTag,
217 | 				{},
218 | 				{ projectRoot: '/test/project' }
219 | 			);
220 | 
221 | 			// Verify readJSON was called with correct parameters
222 | 			expect(mockUtils.readJSON).toHaveBeenCalledWith(
223 | 				testDataPath,
224 | 				'/test/project',
225 | 				sourceTag
226 | 			);
227 | 
228 | 			// Verify writeJSON was called with updated data
229 | 			expect(mockUtils.writeJSON).toHaveBeenCalledWith(
230 | 				testDataPath,
231 | 				expect.objectContaining({
232 | 					backlog: expect.objectContaining({
233 | 						tasks: expect.arrayContaining([
234 | 							expect.objectContaining({ id: 2 }),
235 | 							expect.objectContaining({ id: 3 })
236 | 						])
237 | 					}),
238 | 					'in-progress': expect.objectContaining({
239 | 						tasks: expect.arrayContaining([
240 | 							expect.objectContaining({ id: 4 }),
241 | 							expect.objectContaining({
242 | 								id: 1,
243 | 								tag: 'in-progress'
244 | 							})
245 | 						])
246 | 					})
247 | 				}),
248 | 				'/test/project',
249 | 				null
250 | 			);
251 | 
252 | 			// Verify result structure
253 | 			expect(result).toEqual({
254 | 				message: 'Successfully moved 1 tasks from "backlog" to "in-progress"',
255 | 				movedTasks: [
256 | 					{
257 | 						id: 1,
258 | 						fromTag: 'backlog',
259 | 						toTag: 'in-progress'
260 | 					}
261 | 				]
262 | 			});
263 | 		});
264 | 
265 | 		it('should move multiple tasks between tags', async () => {
266 | 			const taskIds = [1, 3];
267 | 			const sourceTag = 'backlog';
268 | 			const targetTag = 'done';
269 | 
270 | 			const result = await moveTasksBetweenTags(
271 | 				testDataPath,
272 | 				taskIds,
273 | 				sourceTag,
274 | 				targetTag,
275 | 				{},
276 | 				{ projectRoot: '/test/project' }
277 | 			);
278 | 
279 | 			// Verify the moved tasks are in the target tag
280 | 			expect(mockUtils.writeJSON).toHaveBeenCalledWith(
281 | 				testDataPath,
282 | 				expect.objectContaining({
283 | 					backlog: expect.objectContaining({
284 | 						tasks: expect.arrayContaining([expect.objectContaining({ id: 2 })])
285 | 					}),
286 | 					done: expect.objectContaining({
287 | 						tasks: expect.arrayContaining([
288 | 							expect.objectContaining({ id: 5 }),
289 | 							expect.objectContaining({
290 | 								id: 1,
291 | 								tag: 'done'
292 | 							}),
293 | 							expect.objectContaining({
294 | 								id: 3,
295 | 								tag: 'done'
296 | 							})
297 | 						])
298 | 					})
299 | 				}),
300 | 				'/test/project',
301 | 				null
302 | 			);
303 | 
304 | 			// Verify result structure
305 | 			expect(result.movedTasks).toHaveLength(2);
306 | 			expect(result.movedTasks).toEqual(
307 | 				expect.arrayContaining([
308 | 					{ id: 1, fromTag: 'backlog', toTag: 'done' },
309 | 					{ id: 3, fromTag: 'backlog', toTag: 'done' }
310 | 				])
311 | 			);
312 | 		});
313 | 
314 | 		it('should create target tag if it does not exist', async () => {
315 | 			const taskIds = [1];
316 | 			const sourceTag = 'backlog';
317 | 			const targetTag = 'new-tag';
318 | 
319 | 			const result = await moveTasksBetweenTags(
320 | 				testDataPath,
321 | 				taskIds,
322 | 				sourceTag,
323 | 				targetTag,
324 | 				{},
325 | 				{ projectRoot: '/test/project' }
326 | 			);
327 | 
328 | 			// Verify new tag was created
329 | 			expect(mockUtils.writeJSON).toHaveBeenCalledWith(
330 | 				testDataPath,
331 | 				expect.objectContaining({
332 | 					'new-tag': expect.objectContaining({
333 | 						tasks: expect.arrayContaining([
334 | 							expect.objectContaining({
335 | 								id: 1,
336 | 								tag: 'new-tag'
337 | 							})
338 | 						])
339 | 					})
340 | 				}),
341 | 				'/test/project',
342 | 				null
343 | 			);
344 | 		});
345 | 	});
346 | 
347 | 	describe('Dependency Handling', () => {
348 | 		it('should move task with dependencies when withDependencies is true', async () => {
349 | 			const taskIds = [2]; // Task 2 depends on Task 1
350 | 			const sourceTag = 'backlog';
351 | 			const targetTag = 'in-progress';
352 | 
353 | 			const result = await moveTasksBetweenTags(
354 | 				testDataPath,
355 | 				taskIds,
356 | 				sourceTag,
357 | 				targetTag,
358 | 				{ withDependencies: true },
359 | 				{ projectRoot: '/test/project' }
360 | 			);
361 | 
362 | 			// Verify both task 2 and its dependency (task 1) were moved
363 | 			expect(mockUtils.writeJSON).toHaveBeenCalledWith(
364 | 				testDataPath,
365 | 				expect.objectContaining({
366 | 					backlog: expect.objectContaining({
367 | 						tasks: expect.arrayContaining([expect.objectContaining({ id: 3 })])
368 | 					}),
369 | 					'in-progress': expect.objectContaining({
370 | 						tasks: expect.arrayContaining([
371 | 							expect.objectContaining({ id: 4 }),
372 | 							expect.objectContaining({
373 | 								id: 1,
374 | 								tag: 'in-progress'
375 | 							}),
376 | 							expect.objectContaining({
377 | 								id: 2,
378 | 								tag: 'in-progress'
379 | 							})
380 | 						])
381 | 					})
382 | 				}),
383 | 				'/test/project',
384 | 				null
385 | 			);
386 | 		});
387 | 
388 | 		it('should move task normally when ignoreDependencies is true (no cross-tag conflicts to ignore)', async () => {
389 | 			const taskIds = [2]; // Task 2 depends on Task 1
390 | 			const sourceTag = 'backlog';
391 | 			const targetTag = 'in-progress';
392 | 
393 | 			const result = await moveTasksBetweenTags(
394 | 				testDataPath,
395 | 				taskIds,
396 | 				sourceTag,
397 | 				targetTag,
398 | 				{ ignoreDependencies: true },
399 | 				{ projectRoot: '/test/project' }
400 | 			);
401 | 
402 | 			// Since dependencies only exist within tags, there are no cross-tag conflicts to ignore
403 | 			// Task 2 moves with its dependencies intact
404 | 			expect(mockUtils.writeJSON).toHaveBeenCalledWith(
405 | 				testDataPath,
406 | 				expect.objectContaining({
407 | 					backlog: expect.objectContaining({
408 | 						tasks: expect.arrayContaining([
409 | 							expect.objectContaining({ id: 1 }),
410 | 							expect.objectContaining({ id: 3 })
411 | 						])
412 | 					}),
413 | 					'in-progress': expect.objectContaining({
414 | 						tasks: expect.arrayContaining([
415 | 							expect.objectContaining({ id: 4 }),
416 | 							expect.objectContaining({
417 | 								id: 2,
418 | 								tag: 'in-progress',
419 | 								dependencies: [1] // Dependencies preserved since no cross-tag conflicts
420 | 							})
421 | 						])
422 | 					})
423 | 				}),
424 | 				'/test/project',
425 | 				null
426 | 			);
427 | 		});
428 | 
429 | 		it('should provide advisory tips when ignoreDependencies breaks deps', async () => {
430 | 			// Move a task that has dependencies so cross-tag conflicts would be broken
431 | 			const taskIds = [2]; // backlog:2 depends on 1
432 | 			const sourceTag = 'backlog';
433 | 			const targetTag = 'in-progress';
434 | 
435 | 			// Override cross-tag detection to simulate conflicts for this case
436 | 			const depManager = await import(
437 | 				'../../scripts/modules/dependency-manager.js'
438 | 			);
439 | 			depManager.findCrossTagDependencies.mockReturnValueOnce([
440 | 				{ taskId: 2, dependencyId: 1, dependencyTag: sourceTag }
441 | 			]);
442 | 
443 | 			const result = await moveTasksBetweenTags(
444 | 				testDataPath,
445 | 				taskIds,
446 | 				sourceTag,
447 | 				targetTag,
448 | 				{ ignoreDependencies: true },
449 | 				{ projectRoot: '/test/project' }
450 | 			);
451 | 
452 | 			expect(Array.isArray(result.tips)).toBe(true);
453 | 			const expectedTips = [
454 | 				'Run "task-master validate-dependencies" to check for dependency issues.',
455 | 				'Run "task-master fix-dependencies" to automatically repair dangling dependencies.'
456 | 			];
457 | 			expect(result.tips).toHaveLength(expectedTips.length);
458 | 			expect(result.tips).toEqual(expect.arrayContaining(expectedTips));
459 | 		});
460 | 
461 | 		it('should move task without cross-tag dependency conflicts (since dependencies only exist within tags)', async () => {
462 | 			const taskIds = [2]; // Task 2 depends on Task 1 (both in same tag)
463 | 			const sourceTag = 'backlog';
464 | 			const targetTag = 'in-progress';
465 | 
466 | 			// Since dependencies can only exist within the same tag,
467 | 			// there should be no cross-tag conflicts
468 | 			const result = await moveTasksBetweenTags(
469 | 				testDataPath,
470 | 				taskIds,
471 | 				sourceTag,
472 | 				targetTag,
473 | 				{},
474 | 				{ projectRoot: '/test/project' }
475 | 			);
476 | 
477 | 			// Verify task was moved successfully (without dependencies)
478 | 			expect(mockUtils.writeJSON).toHaveBeenCalledWith(
479 | 				testDataPath,
480 | 				expect.objectContaining({
481 | 					backlog: expect.objectContaining({
482 | 						tasks: expect.arrayContaining([
483 | 							expect.objectContaining({ id: 1 }), // Task 1 stays in backlog
484 | 							expect.objectContaining({ id: 3 })
485 | 						])
486 | 					}),
487 | 					'in-progress': expect.objectContaining({
488 | 						tasks: expect.arrayContaining([
489 | 							expect.objectContaining({ id: 4 }),
490 | 							expect.objectContaining({
491 | 								id: 2,
492 | 								tag: 'in-progress'
493 | 							})
494 | 						])
495 | 					})
496 | 				}),
497 | 				'/test/project',
498 | 				null
499 | 			);
500 | 		});
501 | 	});
502 | 
503 | 	describe('Error Handling', () => {
504 | 		it('should throw error for invalid source tag', async () => {
505 | 			const taskIds = [1];
506 | 			const sourceTag = 'nonexistent-tag';
507 | 			const targetTag = 'in-progress';
508 | 
509 | 			// Mock readJSON to return data without the source tag
510 | 			mockUtils.readJSON.mockReturnValue({
511 | 				'in-progress': { tasks: [] }
512 | 			});
513 | 
514 | 			await expect(
515 | 				moveTasksBetweenTags(
516 | 					testDataPath,
517 | 					taskIds,
518 | 					sourceTag,
519 | 					targetTag,
520 | 					{},
521 | 					{ projectRoot: '/test/project' }
522 | 				)
523 | 			).rejects.toThrow('Source tag "nonexistent-tag" not found or invalid');
524 | 		});
525 | 
526 | 		it('should throw error for invalid task IDs', async () => {
527 | 			const taskIds = [999]; // Non-existent task ID
528 | 			const sourceTag = 'backlog';
529 | 			const targetTag = 'in-progress';
530 | 
531 | 			await expect(
532 | 				moveTasksBetweenTags(
533 | 					testDataPath,
534 | 					taskIds,
535 | 					sourceTag,
536 | 					targetTag,
537 | 					{},
538 | 					{ projectRoot: '/test/project' }
539 | 				)
540 | 			).rejects.toThrow('Task 999 not found in source tag "backlog"');
541 | 		});
542 | 
543 | 		it('should throw error for subtask movement', async () => {
544 | 			const taskIds = ['1.1']; // Subtask ID
545 | 			const sourceTag = 'backlog';
546 | 			const targetTag = 'in-progress';
547 | 
548 | 			await expect(
549 | 				moveTasksBetweenTags(
550 | 					testDataPath,
551 | 					taskIds,
552 | 					sourceTag,
553 | 					targetTag,
554 | 					{},
555 | 					{ projectRoot: '/test/project' }
556 | 				)
557 | 			).rejects.toThrow('Cannot move subtasks directly between tags');
558 | 		});
559 | 
560 | 		it('should handle ID conflicts in target tag', async () => {
561 | 			// Setup data with conflicting IDs
562 | 			const conflictingData = {
563 | 				backlog: {
564 | 					tasks: [
565 | 						{
566 | 							id: 1,
567 | 							title: 'Backlog Task',
568 | 							tag: 'backlog'
569 | 						}
570 | 					]
571 | 				},
572 | 				'in-progress': {
573 | 					tasks: [
574 | 						{
575 | 							id: 1, // Same ID as in backlog
576 | 							title: 'In Progress Task',
577 | 							tag: 'in-progress'
578 | 						}
579 | 					]
580 | 				}
581 | 			};
582 | 
583 | 			mockUtils.readJSON.mockReturnValue(conflictingData);
584 | 
585 | 			const taskIds = [1];
586 | 			const sourceTag = 'backlog';
587 | 			const targetTag = 'in-progress';
588 | 
589 | 			await expect(
590 | 				moveTasksBetweenTags(
591 | 					testDataPath,
592 | 					taskIds,
593 | 					sourceTag,
594 | 					targetTag,
595 | 					{},
596 | 					{ projectRoot: '/test/project' }
597 | 				)
598 | 			).rejects.toThrow('Task 1 already exists in target tag "in-progress"');
599 | 
600 | 			// Validate suggestions on the error payload
601 | 			try {
602 | 				await moveTasksBetweenTags(
603 | 					testDataPath,
604 | 					taskIds,
605 | 					sourceTag,
606 | 					targetTag,
607 | 					{},
608 | 					{ projectRoot: '/test/project' }
609 | 				);
610 | 			} catch (err) {
611 | 				expect(err.code).toBe('TASK_ALREADY_EXISTS');
612 | 				expect(Array.isArray(err.data?.suggestions)).toBe(true);
613 | 				const s = (err.data?.suggestions || []).join(' ');
614 | 				expect(s).toContain('different target tag');
615 | 				expect(s).toContain('different set of IDs');
616 | 				expect(s).toContain('within-tag');
617 | 			}
618 | 		});
619 | 	});
620 | 
621 | 	describe('Edge Cases', () => {
622 | 		it('should handle empty task list in source tag', async () => {
623 | 			const emptyData = {
624 | 				backlog: { tasks: [] },
625 | 				'in-progress': { tasks: [] }
626 | 			};
627 | 
628 | 			mockUtils.readJSON.mockReturnValue(emptyData);
629 | 
630 | 			const taskIds = [1];
631 | 			const sourceTag = 'backlog';
632 | 			const targetTag = 'in-progress';
633 | 
634 | 			await expect(
635 | 				moveTasksBetweenTags(
636 | 					testDataPath,
637 | 					taskIds,
638 | 					sourceTag,
639 | 					targetTag,
640 | 					{},
641 | 					{ projectRoot: '/test/project' }
642 | 				)
643 | 			).rejects.toThrow('Task 1 not found in source tag "backlog"');
644 | 		});
645 | 
646 | 		it('should preserve task metadata during move', async () => {
647 | 			const taskIds = [1];
648 | 			const sourceTag = 'backlog';
649 | 			const targetTag = 'in-progress';
650 | 
651 | 			const result = await moveTasksBetweenTags(
652 | 				testDataPath,
653 | 				taskIds,
654 | 				sourceTag,
655 | 				targetTag,
656 | 				{},
657 | 				{ projectRoot: '/test/project' }
658 | 			);
659 | 
660 | 			// Verify task metadata is preserved
661 | 			expect(mockUtils.writeJSON).toHaveBeenCalledWith(
662 | 				testDataPath,
663 | 				expect.objectContaining({
664 | 					'in-progress': expect.objectContaining({
665 | 						tasks: expect.arrayContaining([
666 | 							expect.objectContaining({
667 | 								id: 1,
668 | 								title: 'Backlog Task 1',
669 | 								description: 'A task in backlog',
670 | 								status: 'pending',
671 | 								priority: 'medium',
672 | 								tag: 'in-progress', // Tag should be updated
673 | 								metadata: expect.objectContaining({
674 | 									moveHistory: expect.arrayContaining([
675 | 										expect.objectContaining({
676 | 											fromTag: 'backlog',
677 | 											toTag: 'in-progress',
678 | 											timestamp: expect.any(String)
679 | 										})
680 | 									])
681 | 								})
682 | 							})
683 | 						])
684 | 					})
685 | 				}),
686 | 				'/test/project',
687 | 				null
688 | 			);
689 | 		});
690 | 
691 | 		// Note: force flag deprecated for cross-tag moves; covered by with/ignore dependencies tests
692 | 	});
693 | 
694 | 	describe('Complex Scenarios', () => {
695 | 		it('should handle complex moves without cross-tag conflicts (dependencies only within tags)', async () => {
696 | 			// Setup data with valid within-tag dependencies
697 | 			const validData = {
698 | 				backlog: {
699 | 					tasks: [
700 | 						{
701 | 							id: 1,
702 | 							title: 'Task 1',
703 | 							dependencies: [], // No dependencies
704 | 							tag: 'backlog'
705 | 						},
706 | 						{
707 | 							id: 3,
708 | 							title: 'Task 3',
709 | 							dependencies: [1], // Depends on Task 1 (same tag)
710 | 							tag: 'backlog'
711 | 						}
712 | 					]
713 | 				},
714 | 				'in-progress': {
715 | 					tasks: [
716 | 						{
717 | 							id: 2,
718 | 							title: 'Task 2',
719 | 							dependencies: [], // No dependencies
720 | 							tag: 'in-progress'
721 | 						}
722 | 					]
723 | 				}
724 | 			};
725 | 
726 | 			mockUtils.readJSON.mockReturnValue(validData);
727 | 
728 | 			const taskIds = [3];
729 | 			const sourceTag = 'backlog';
730 | 			const targetTag = 'in-progress';
731 | 
732 | 			// Should succeed since there are no cross-tag conflicts
733 | 			const result = await moveTasksBetweenTags(
734 | 				testDataPath,
735 | 				taskIds,
736 | 				sourceTag,
737 | 				targetTag,
738 | 				{},
739 | 				{ projectRoot: '/test/project' }
740 | 			);
741 | 
742 | 			expect(result).toEqual({
743 | 				message: 'Successfully moved 1 tasks from "backlog" to "in-progress"',
744 | 				movedTasks: [{ id: 3, fromTag: 'backlog', toTag: 'in-progress' }]
745 | 			});
746 | 		});
747 | 
748 | 		it('should handle bulk move with mixed dependency scenarios', async () => {
749 | 			const taskIds = [1, 2, 3]; // Multiple tasks with dependencies
750 | 			const sourceTag = 'backlog';
751 | 			const targetTag = 'in-progress';
752 | 
753 | 			const result = await moveTasksBetweenTags(
754 | 				testDataPath,
755 | 				taskIds,
756 | 				sourceTag,
757 | 				targetTag,
758 | 				{ withDependencies: true },
759 | 				{ projectRoot: '/test/project' }
760 | 			);
761 | 
762 | 			// Verify all tasks were moved
763 | 			expect(mockUtils.writeJSON).toHaveBeenCalledWith(
764 | 				testDataPath,
765 | 				expect.objectContaining({
766 | 					backlog: expect.objectContaining({
767 | 						tasks: [] // All tasks should be moved
768 | 					}),
769 | 					'in-progress': expect.objectContaining({
770 | 						tasks: expect.arrayContaining([
771 | 							expect.objectContaining({ id: 4 }),
772 | 							expect.objectContaining({ id: 1, tag: 'in-progress' }),
773 | 							expect.objectContaining({ id: 2, tag: 'in-progress' }),
774 | 							expect.objectContaining({ id: 3, tag: 'in-progress' })
775 | 						])
776 | 					})
777 | 				}),
778 | 				'/test/project',
779 | 				null
780 | 			);
781 | 
782 | 			// Verify result structure
783 | 			expect(result.movedTasks).toHaveLength(3);
784 | 			expect(result.movedTasks).toEqual(
785 | 				expect.arrayContaining([
786 | 					{ id: 1, fromTag: 'backlog', toTag: 'in-progress' },
787 | 					{ id: 2, fromTag: 'backlog', toTag: 'in-progress' },
788 | 					{ id: 3, fromTag: 'backlog', toTag: 'in-progress' }
789 | 				])
790 | 			);
791 | 		});
792 | 	});
793 | });
794 | 
```

--------------------------------------------------------------------------------
/tests/unit/ai-providers/gemini-cli.test.js:
--------------------------------------------------------------------------------

```javascript
  1 | import { jest } from '@jest/globals';
  2 | 
  3 | // Mock the ai module
  4 | jest.unstable_mockModule('ai', () => ({
  5 | 	generateObject: jest.fn(),
  6 | 	generateText: jest.fn(),
  7 | 	streamText: jest.fn()
  8 | }));
  9 | 
 10 | // Mock the gemini-cli SDK module
 11 | jest.unstable_mockModule('ai-sdk-provider-gemini-cli', () => ({
 12 | 	createGeminiProvider: jest.fn((options) => {
 13 | 		const provider = (modelId, settings) => ({
 14 | 			// Mock language model
 15 | 			id: modelId,
 16 | 			settings,
 17 | 			authOptions: options
 18 | 		});
 19 | 		provider.languageModel = jest.fn((id, settings) => ({ id, settings }));
 20 | 		provider.chat = provider.languageModel;
 21 | 		return provider;
 22 | 	})
 23 | }));
 24 | 
 25 | // Mock the base provider
 26 | jest.unstable_mockModule('../../../src/ai-providers/base-provider.js', () => ({
 27 | 	BaseAIProvider: class {
 28 | 		constructor() {
 29 | 			this.name = 'Base Provider';
 30 | 		}
 31 | 		handleError(context, error) {
 32 | 			throw error;
 33 | 		}
 34 | 		validateParams(params) {
 35 | 			// Basic validation
 36 | 			if (!params.modelId) {
 37 | 				throw new Error('Model ID is required');
 38 | 			}
 39 | 		}
 40 | 		validateMessages(messages) {
 41 | 			if (!messages || !Array.isArray(messages)) {
 42 | 				throw new Error('Invalid messages array');
 43 | 			}
 44 | 		}
 45 | 		async generateObject(params) {
 46 | 			// Mock implementation that can be overridden
 47 | 			throw new Error('Mock base generateObject error');
 48 | 		}
 49 | 	}
 50 | }));
 51 | 
 52 | // Mock the log module
 53 | jest.unstable_mockModule('../../../scripts/modules/utils.js', () => ({
 54 | 	log: jest.fn()
 55 | }));
 56 | 
 57 | // Import after mocking
 58 | const { GeminiCliProvider } = await import(
 59 | 	'../../../src/ai-providers/gemini-cli.js'
 60 | );
 61 | const { createGeminiProvider } = await import('ai-sdk-provider-gemini-cli');
 62 | const { generateObject, generateText, streamText } = await import('ai');
 63 | const { log } = await import('../../../scripts/modules/utils.js');
 64 | 
 65 | describe('GeminiCliProvider', () => {
 66 | 	let provider;
 67 | 	let consoleLogSpy;
 68 | 
 69 | 	beforeEach(() => {
 70 | 		provider = new GeminiCliProvider();
 71 | 		jest.clearAllMocks();
 72 | 		consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
 73 | 	});
 74 | 
 75 | 	afterEach(() => {
 76 | 		consoleLogSpy.mockRestore();
 77 | 	});
 78 | 
 79 | 	describe('constructor', () => {
 80 | 		it('should set the provider name to Gemini CLI', () => {
 81 | 			expect(provider.name).toBe('Gemini CLI');
 82 | 		});
 83 | 	});
 84 | 
 85 | 	describe('validateAuth', () => {
 86 | 		it('should not throw an error when API key is provided', () => {
 87 | 			expect(() => provider.validateAuth({ apiKey: 'test-key' })).not.toThrow();
 88 | 			expect(consoleLogSpy).not.toHaveBeenCalled();
 89 | 		});
 90 | 
 91 | 		it('should not require API key and should not log messages', () => {
 92 | 			expect(() => provider.validateAuth({})).not.toThrow();
 93 | 			expect(consoleLogSpy).not.toHaveBeenCalled();
 94 | 		});
 95 | 
 96 | 		it('should not require any parameters', () => {
 97 | 			expect(() => provider.validateAuth()).not.toThrow();
 98 | 			expect(consoleLogSpy).not.toHaveBeenCalled();
 99 | 		});
100 | 	});
101 | 
102 | 	describe('getClient', () => {
103 | 		it('should return a gemini client with API key auth when apiKey is provided', async () => {
104 | 			const client = await provider.getClient({ apiKey: 'test-api-key' });
105 | 
106 | 			expect(client).toBeDefined();
107 | 			expect(typeof client).toBe('function');
108 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
109 | 				authType: 'api-key',
110 | 				apiKey: 'test-api-key'
111 | 			});
112 | 		});
113 | 
114 | 		it('should return a gemini client with OAuth auth when no apiKey is provided', async () => {
115 | 			const client = await provider.getClient({});
116 | 
117 | 			expect(client).toBeDefined();
118 | 			expect(typeof client).toBe('function');
119 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
120 | 				authType: 'oauth-personal'
121 | 			});
122 | 		});
123 | 
124 | 		it('should include baseURL when provided', async () => {
125 | 			const client = await provider.getClient({
126 | 				apiKey: 'test-key',
127 | 				baseURL: 'https://custom-endpoint.com'
128 | 			});
129 | 
130 | 			expect(client).toBeDefined();
131 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
132 | 				authType: 'api-key',
133 | 				apiKey: 'test-key',
134 | 				baseURL: 'https://custom-endpoint.com'
135 | 			});
136 | 		});
137 | 
138 | 		it('should have languageModel and chat methods', async () => {
139 | 			const client = await provider.getClient({ apiKey: 'test-key' });
140 | 			expect(client.languageModel).toBeDefined();
141 | 			expect(client.chat).toBeDefined();
142 | 			expect(client.chat).toBe(client.languageModel);
143 | 		});
144 | 	});
145 | 
146 | 	describe('_extractSystemMessage', () => {
147 | 		it('should extract single system message', () => {
148 | 			const messages = [
149 | 				{ role: 'system', content: 'You are a helpful assistant' },
150 | 				{ role: 'user', content: 'Hello' }
151 | 			];
152 | 			const result = provider._extractSystemMessage(messages);
153 | 			expect(result.systemPrompt).toBe('You are a helpful assistant');
154 | 			expect(result.messages).toEqual([{ role: 'user', content: 'Hello' }]);
155 | 		});
156 | 
157 | 		it('should combine multiple system messages', () => {
158 | 			const messages = [
159 | 				{ role: 'system', content: 'You are helpful' },
160 | 				{ role: 'system', content: 'Be concise' },
161 | 				{ role: 'user', content: 'Hello' }
162 | 			];
163 | 			const result = provider._extractSystemMessage(messages);
164 | 			expect(result.systemPrompt).toBe('You are helpful\n\nBe concise');
165 | 			expect(result.messages).toEqual([{ role: 'user', content: 'Hello' }]);
166 | 		});
167 | 
168 | 		it('should handle messages without system prompts', () => {
169 | 			const messages = [
170 | 				{ role: 'user', content: 'Hello' },
171 | 				{ role: 'assistant', content: 'Hi there' }
172 | 			];
173 | 			const result = provider._extractSystemMessage(messages);
174 | 			expect(result.systemPrompt).toBeUndefined();
175 | 			expect(result.messages).toEqual(messages);
176 | 		});
177 | 
178 | 		it('should handle empty or invalid input', () => {
179 | 			expect(provider._extractSystemMessage([])).toEqual({
180 | 				systemPrompt: undefined,
181 | 				messages: []
182 | 			});
183 | 			expect(provider._extractSystemMessage(null)).toEqual({
184 | 				systemPrompt: undefined,
185 | 				messages: []
186 | 			});
187 | 			expect(provider._extractSystemMessage(undefined)).toEqual({
188 | 				systemPrompt: undefined,
189 | 				messages: []
190 | 			});
191 | 		});
192 | 
193 | 		it('should add JSON enforcement when enforceJsonOutput is true', () => {
194 | 			const messages = [
195 | 				{ role: 'system', content: 'You are a helpful assistant' },
196 | 				{ role: 'user', content: 'Hello' }
197 | 			];
198 | 			const result = provider._extractSystemMessage(messages, {
199 | 				enforceJsonOutput: true
200 | 			});
201 | 			expect(result.systemPrompt).toContain('You are a helpful assistant');
202 | 			expect(result.systemPrompt).toContain(
203 | 				'CRITICAL: You MUST respond with ONLY valid JSON'
204 | 			);
205 | 			expect(result.messages).toEqual([{ role: 'user', content: 'Hello' }]);
206 | 		});
207 | 
208 | 		it('should add JSON enforcement with no existing system message', () => {
209 | 			const messages = [{ role: 'user', content: 'Return JSON format' }];
210 | 			const result = provider._extractSystemMessage(messages, {
211 | 				enforceJsonOutput: true
212 | 			});
213 | 			expect(result.systemPrompt).toBe(
214 | 				'CRITICAL: You MUST respond with ONLY valid JSON. Do not include any explanatory text, markdown formatting, code block markers, or conversational phrases like "Here is" or "Of course". Your entire response must be parseable JSON that starts with { or [ and ends with } or ]. No exceptions.'
215 | 			);
216 | 			expect(result.messages).toEqual([
217 | 				{ role: 'user', content: 'Return JSON format' }
218 | 			]);
219 | 		});
220 | 	});
221 | 
222 | 	describe('_detectJsonRequest', () => {
223 | 		it('should detect JSON requests from user messages', () => {
224 | 			const messages = [
225 | 				{
226 | 					role: 'user',
227 | 					content: 'Please return JSON format with subtasks array'
228 | 				}
229 | 			];
230 | 			expect(provider._detectJsonRequest(messages)).toBe(true);
231 | 		});
232 | 
233 | 		it('should detect various JSON indicators', () => {
234 | 			const testCases = [
235 | 				'respond only with valid JSON',
236 | 				'return JSON format',
237 | 				'output schema: {"test": true}',
238 | 				'format: [{"id": 1}]',
239 | 				'Please return subtasks in array format',
240 | 				'Return an object with properties'
241 | 			];
242 | 
243 | 			testCases.forEach((content) => {
244 | 				const messages = [{ role: 'user', content }];
245 | 				expect(provider._detectJsonRequest(messages)).toBe(true);
246 | 			});
247 | 		});
248 | 
249 | 		it('should not detect JSON requests for regular conversation', () => {
250 | 			const messages = [{ role: 'user', content: 'Hello, how are you today?' }];
251 | 			expect(provider._detectJsonRequest(messages)).toBe(false);
252 | 		});
253 | 
254 | 		it('should handle multiple user messages', () => {
255 | 			const messages = [
256 | 				{ role: 'user', content: 'Hello' },
257 | 				{ role: 'assistant', content: 'Hi there' },
258 | 				{ role: 'user', content: 'Now please return JSON format' }
259 | 			];
260 | 			expect(provider._detectJsonRequest(messages)).toBe(true);
261 | 		});
262 | 	});
263 | 
264 | 	describe('_getJsonEnforcementPrompt', () => {
265 | 		it('should return strict JSON enforcement prompt', () => {
266 | 			const prompt = provider._getJsonEnforcementPrompt();
267 | 			expect(prompt).toContain('CRITICAL');
268 | 			expect(prompt).toContain('ONLY valid JSON');
269 | 			expect(prompt).toContain('No exceptions');
270 | 		});
271 | 	});
272 | 
273 | 	describe('_isValidJson', () => {
274 | 		it('should return true for valid JSON objects', () => {
275 | 			expect(provider._isValidJson('{"test": true}')).toBe(true);
276 | 			expect(provider._isValidJson('{"subtasks": [{"id": 1}]}')).toBe(true);
277 | 		});
278 | 
279 | 		it('should return true for valid JSON arrays', () => {
280 | 			expect(provider._isValidJson('[1, 2, 3]')).toBe(true);
281 | 			expect(provider._isValidJson('[{"id": 1}, {"id": 2}]')).toBe(true);
282 | 		});
283 | 
284 | 		it('should return false for invalid JSON', () => {
285 | 			expect(provider._isValidJson('Of course. Here is...')).toBe(false);
286 | 			expect(provider._isValidJson('{"invalid": json}')).toBe(false);
287 | 			expect(provider._isValidJson('not json at all')).toBe(false);
288 | 		});
289 | 
290 | 		it('should handle edge cases', () => {
291 | 			expect(provider._isValidJson('')).toBe(false);
292 | 			expect(provider._isValidJson(null)).toBe(false);
293 | 			expect(provider._isValidJson(undefined)).toBe(false);
294 | 			expect(provider._isValidJson('   {"test": true}   ')).toBe(true); // with whitespace
295 | 		});
296 | 	});
297 | 
298 | 	describe('extractJson', () => {
299 | 		it('should extract JSON from markdown code blocks', () => {
300 | 			const input = '```json\n{"subtasks": [{"id": 1}]}\n```';
301 | 			const result = provider.extractJson(input);
302 | 			const parsed = JSON.parse(result);
303 | 			expect(parsed).toEqual({ subtasks: [{ id: 1 }] });
304 | 		});
305 | 
306 | 		it('should extract JSON with explanatory text', () => {
307 | 			const input = 'Here\'s the JSON response:\n{"subtasks": [{"id": 1}]}';
308 | 			const result = provider.extractJson(input);
309 | 			const parsed = JSON.parse(result);
310 | 			expect(parsed).toEqual({ subtasks: [{ id: 1 }] });
311 | 		});
312 | 
313 | 		it('should handle variable declarations', () => {
314 | 			const input = 'const result = {"subtasks": [{"id": 1}]};';
315 | 			const result = provider.extractJson(input);
316 | 			const parsed = JSON.parse(result);
317 | 			expect(parsed).toEqual({ subtasks: [{ id: 1 }] });
318 | 		});
319 | 
320 | 		it('should handle trailing commas with jsonc-parser', () => {
321 | 			const input = '{"subtasks": [{"id": 1,}],}';
322 | 			const result = provider.extractJson(input);
323 | 			const parsed = JSON.parse(result);
324 | 			expect(parsed).toEqual({ subtasks: [{ id: 1 }] });
325 | 		});
326 | 
327 | 		it('should handle arrays', () => {
328 | 			const input = 'The result is: [1, 2, 3]';
329 | 			const result = provider.extractJson(input);
330 | 			const parsed = JSON.parse(result);
331 | 			expect(parsed).toEqual([1, 2, 3]);
332 | 		});
333 | 
334 | 		it('should handle nested objects with proper bracket matching', () => {
335 | 			const input =
336 | 				'Response: {"outer": {"inner": {"value": "test"}}} extra text';
337 | 			const result = provider.extractJson(input);
338 | 			const parsed = JSON.parse(result);
339 | 			expect(parsed).toEqual({ outer: { inner: { value: 'test' } } });
340 | 		});
341 | 
342 | 		it('should handle escaped quotes in strings', () => {
343 | 			const input = '{"message": "He said \\"hello\\" to me"}';
344 | 			const result = provider.extractJson(input);
345 | 			const parsed = JSON.parse(result);
346 | 			expect(parsed).toEqual({ message: 'He said "hello" to me' });
347 | 		});
348 | 
349 | 		it('should return original text if no JSON found', () => {
350 | 			const input = 'No JSON here';
351 | 			expect(provider.extractJson(input)).toBe(input);
352 | 		});
353 | 
354 | 		it('should handle null or non-string input', () => {
355 | 			expect(provider.extractJson(null)).toBe(null);
356 | 			expect(provider.extractJson(undefined)).toBe(undefined);
357 | 			expect(provider.extractJson(123)).toBe(123);
358 | 		});
359 | 
360 | 		it('should handle partial JSON by finding valid boundaries', () => {
361 | 			const input = '{"valid": true, "partial": "incomplete';
362 | 			// Should return original text since no valid JSON can be extracted
363 | 			expect(provider.extractJson(input)).toBe(input);
364 | 		});
365 | 
366 | 		it('should handle performance edge cases with large text', () => {
367 | 			// Test with large text that has JSON at the end
368 | 			const largePrefix = 'This is a very long explanation. '.repeat(1000);
369 | 			const json = '{"result": "success"}';
370 | 			const input = largePrefix + json;
371 | 
372 | 			const result = provider.extractJson(input);
373 | 			const parsed = JSON.parse(result);
374 | 			expect(parsed).toEqual({ result: 'success' });
375 | 		});
376 | 
377 | 		it('should handle early termination for very large invalid content', () => {
378 | 			// Test that it doesn't hang on very large content without JSON
379 | 			const largeText = 'No JSON here. '.repeat(2000);
380 | 			const result = provider.extractJson(largeText);
381 | 			expect(result).toBe(largeText);
382 | 		});
383 | 	});
384 | 
385 | 	describe('generateObject', () => {
386 | 		const mockParams = {
387 | 			modelId: 'gemini-2.0-flash-exp',
388 | 			apiKey: 'test-key',
389 | 			messages: [{ role: 'user', content: 'Test message' }],
390 | 			schema: { type: 'object', properties: {} },
391 | 			objectName: 'testObject'
392 | 		};
393 | 
394 | 		beforeEach(() => {
395 | 			jest.clearAllMocks();
396 | 		});
397 | 
398 | 		it('should handle JSON parsing errors by attempting manual extraction', async () => {
399 | 			// Mock the parent generateObject to throw a JSON parsing error
400 | 			jest
401 | 				.spyOn(
402 | 					Object.getPrototypeOf(Object.getPrototypeOf(provider)),
403 | 					'generateObject'
404 | 				)
405 | 				.mockRejectedValueOnce(new Error('Failed to parse JSON response'));
406 | 
407 | 			// Mock generateObject from ai module to return text with JSON
408 | 			generateObject.mockResolvedValueOnce({
409 | 				rawResponse: {
410 | 					text: 'Here is the JSON:\n```json\n{"subtasks": [{"id": 1}]}\n```'
411 | 				},
412 | 				object: null,
413 | 				usage: { promptTokens: 10, completionTokens: 20, totalTokens: 30 }
414 | 			});
415 | 
416 | 			const result = await provider.generateObject(mockParams);
417 | 
418 | 			expect(log).toHaveBeenCalledWith(
419 | 				'debug',
420 | 				expect.stringContaining('attempting manual extraction')
421 | 			);
422 | 			expect(generateObject).toHaveBeenCalledWith({
423 | 				model: expect.objectContaining({
424 | 					id: 'gemini-2.0-flash-exp',
425 | 					authOptions: expect.objectContaining({
426 | 						authType: 'api-key',
427 | 						apiKey: 'test-key'
428 | 					})
429 | 				}),
430 | 				messages: mockParams.messages,
431 | 				schema: mockParams.schema,
432 | 				mode: 'json', // Should use json mode for Gemini
433 | 				system: expect.stringContaining(
434 | 					'CRITICAL: You MUST respond with ONLY valid JSON'
435 | 				),
436 | 				maxTokens: undefined,
437 | 				temperature: undefined
438 | 			});
439 | 			expect(result.object).toEqual({ subtasks: [{ id: 1 }] });
440 | 		});
441 | 
442 | 		it('should throw error if manual extraction also fails', async () => {
443 | 			// Mock parent to throw JSON error
444 | 			jest
445 | 				.spyOn(
446 | 					Object.getPrototypeOf(Object.getPrototypeOf(provider)),
447 | 					'generateObject'
448 | 				)
449 | 				.mockRejectedValueOnce(new Error('Failed to parse JSON'));
450 | 
451 | 			// Mock generateObject to return unparseable text
452 | 			generateObject.mockResolvedValueOnce({
453 | 				rawResponse: { text: 'Not valid JSON at all' },
454 | 				object: null
455 | 			});
456 | 
457 | 			await expect(provider.generateObject(mockParams)).rejects.toThrow(
458 | 				'Gemini CLI failed to generate valid JSON object: Failed to parse JSON'
459 | 			);
460 | 		});
461 | 
462 | 		it('should pass through non-JSON errors unchanged', async () => {
463 | 			const otherError = new Error('Network error');
464 | 			jest
465 | 				.spyOn(
466 | 					Object.getPrototypeOf(Object.getPrototypeOf(provider)),
467 | 					'generateObject'
468 | 				)
469 | 				.mockRejectedValueOnce(otherError);
470 | 
471 | 			await expect(provider.generateObject(mockParams)).rejects.toThrow(
472 | 				'Network error'
473 | 			);
474 | 			expect(generateObject).not.toHaveBeenCalled();
475 | 		});
476 | 
477 | 		it('should handle successful response from parent', async () => {
478 | 			const mockResult = {
479 | 				object: { test: 'data' },
480 | 				usage: { inputTokens: 5, outputTokens: 10, totalTokens: 15 }
481 | 			};
482 | 			jest
483 | 				.spyOn(
484 | 					Object.getPrototypeOf(Object.getPrototypeOf(provider)),
485 | 					'generateObject'
486 | 				)
487 | 				.mockResolvedValueOnce(mockResult);
488 | 
489 | 			const result = await provider.generateObject(mockParams);
490 | 			expect(result).toEqual(mockResult);
491 | 			expect(generateObject).not.toHaveBeenCalled();
492 | 		});
493 | 	});
494 | 
495 | 	describe('system message support', () => {
496 | 		const mockParams = {
497 | 			modelId: 'gemini-2.0-flash-exp',
498 | 			apiKey: 'test-key',
499 | 			messages: [
500 | 				{ role: 'system', content: 'You are a helpful assistant' },
501 | 				{ role: 'user', content: 'Hello' }
502 | 			],
503 | 			maxTokens: 100,
504 | 			temperature: 0.7
505 | 		};
506 | 
507 | 		describe('generateText with system messages', () => {
508 | 			beforeEach(() => {
509 | 				jest.clearAllMocks();
510 | 			});
511 | 
512 | 			it('should pass system prompt separately to AI SDK', async () => {
513 | 				const { generateText } = await import('ai');
514 | 				generateText.mockResolvedValueOnce({
515 | 					text: 'Hello! How can I help you?',
516 | 					usage: { promptTokens: 10, completionTokens: 8, totalTokens: 18 }
517 | 				});
518 | 
519 | 				const result = await provider.generateText(mockParams);
520 | 
521 | 				expect(generateText).toHaveBeenCalledWith({
522 | 					model: expect.objectContaining({
523 | 						id: 'gemini-2.0-flash-exp'
524 | 					}),
525 | 					system: 'You are a helpful assistant',
526 | 					messages: [{ role: 'user', content: 'Hello' }],
527 | 					maxTokens: 100,
528 | 					temperature: 0.7
529 | 				});
530 | 				expect(result.text).toBe('Hello! How can I help you?');
531 | 			});
532 | 
533 | 			it('should handle messages without system prompt', async () => {
534 | 				const { generateText } = await import('ai');
535 | 				const paramsNoSystem = {
536 | 					...mockParams,
537 | 					messages: [{ role: 'user', content: 'Hello' }]
538 | 				};
539 | 
540 | 				generateText.mockResolvedValueOnce({
541 | 					text: 'Hi there!',
542 | 					usage: { promptTokens: 5, completionTokens: 3, totalTokens: 8 }
543 | 				});
544 | 
545 | 				await provider.generateText(paramsNoSystem);
546 | 
547 | 				expect(generateText).toHaveBeenCalledWith({
548 | 					model: expect.objectContaining({
549 | 						id: 'gemini-2.0-flash-exp'
550 | 					}),
551 | 					system: undefined,
552 | 					messages: [{ role: 'user', content: 'Hello' }],
553 | 					maxTokens: 100,
554 | 					temperature: 0.7
555 | 				});
556 | 			});
557 | 		});
558 | 
559 | 		describe('streamText with system messages', () => {
560 | 			it('should pass system prompt separately to AI SDK', async () => {
561 | 				const { streamText } = await import('ai');
562 | 				const mockStream = { stream: 'mock-stream' };
563 | 				streamText.mockResolvedValueOnce(mockStream);
564 | 
565 | 				const result = await provider.streamText(mockParams);
566 | 
567 | 				expect(streamText).toHaveBeenCalledWith({
568 | 					model: expect.objectContaining({
569 | 						id: 'gemini-2.0-flash-exp'
570 | 					}),
571 | 					system: 'You are a helpful assistant',
572 | 					messages: [{ role: 'user', content: 'Hello' }],
573 | 					maxTokens: 100,
574 | 					temperature: 0.7
575 | 				});
576 | 				expect(result).toBe(mockStream);
577 | 			});
578 | 		});
579 | 
580 | 		describe('generateObject with system messages', () => {
581 | 			const mockObjectParams = {
582 | 				...mockParams,
583 | 				schema: { type: 'object', properties: {} },
584 | 				objectName: 'testObject'
585 | 			};
586 | 
587 | 			it('should include system prompt in fallback generateObject call', async () => {
588 | 				// Mock parent to throw JSON error
589 | 				jest
590 | 					.spyOn(
591 | 						Object.getPrototypeOf(Object.getPrototypeOf(provider)),
592 | 						'generateObject'
593 | 					)
594 | 					.mockRejectedValueOnce(new Error('Failed to parse JSON'));
595 | 
596 | 				// Mock direct generateObject call
597 | 				generateObject.mockResolvedValueOnce({
598 | 					object: { result: 'success' },
599 | 					usage: { promptTokens: 15, completionTokens: 10, totalTokens: 25 }
600 | 				});
601 | 
602 | 				const result = await provider.generateObject(mockObjectParams);
603 | 
604 | 				expect(generateObject).toHaveBeenCalledWith({
605 | 					model: expect.objectContaining({
606 | 						id: 'gemini-2.0-flash-exp'
607 | 					}),
608 | 					system: expect.stringContaining('You are a helpful assistant'),
609 | 					messages: [{ role: 'user', content: 'Hello' }],
610 | 					schema: mockObjectParams.schema,
611 | 					mode: 'json',
612 | 					maxTokens: 100,
613 | 					temperature: 0.7
614 | 				});
615 | 				expect(result.object).toEqual({ result: 'success' });
616 | 			});
617 | 		});
618 | 	});
619 | 
620 | 	// Note: Error handling for module loading is tested in integration tests
621 | 	// since dynamic imports are difficult to mock properly in unit tests
622 | 
623 | 	describe('authentication scenarios', () => {
624 | 		it('should use api-key auth type with API key', async () => {
625 | 			await provider.getClient({ apiKey: 'gemini-test-key' });
626 | 
627 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
628 | 				authType: 'api-key',
629 | 				apiKey: 'gemini-test-key'
630 | 			});
631 | 		});
632 | 
633 | 		it('should use oauth-personal auth type without API key', async () => {
634 | 			await provider.getClient({});
635 | 
636 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
637 | 				authType: 'oauth-personal'
638 | 			});
639 | 		});
640 | 
641 | 		it('should handle empty string API key as no API key', async () => {
642 | 			await provider.getClient({ apiKey: '' });
643 | 
644 | 			expect(createGeminiProvider).toHaveBeenCalledWith({
645 | 				authType: 'oauth-personal'
646 | 			});
647 | 		});
648 | 	});
649 | });
650 | 
```
Page 33/52FirstPrevNextLast